
本文介绍如何将时间复杂度从 o(b) 降至 o(n) 来解决 hackerrank 上的“movement slots”问题——通过检测状态序列中的循环节(intro + cycle),避免对超大步数 b(如 10¹²)进行暴力模拟。
本文介绍如何将时间复杂度从 o(b) 降至 o(n) 来解决 hackerrank 上的“movement slots”问题——通过检测状态序列中的循环节(intro + cycle),避免对超大步数 b(如 10¹²)进行暴力模拟。
在轮盘弹跳问题中,一个球从初始槽位 a 出发,每经过一次弹跳,就根据当前槽位 i 对应的偏移量 slots[i] 跳转到新位置:
$$
\text{next} = (i + \text{slots}[i]) \bmod n
$$
其中 n 是槽位总数(索引为 0 到 n-1)。当查询次数 q 较少但单次弹跳步数 b 极大(例如 $10^{12}$)时,朴素的 for _ in range(b) 模拟必然超时。
根本优化思路是:状态空间有限(仅 n 个可能槽位),因此球的位置序列必在有限步内进入循环。该序列结构为经典的「ρ 形」(Rho-shaped):一段非重复的前导(intro)后接一个纯循环(cycle)。例如:
a → s₁ → s₂ → … → sₓ → c₁ → c₂ → … → c_y → c₁ → …
↑_____________↑
cycle starts here- X:intro 长度(首次出现重复前的步数)
- Y:cycle 长度(循环节周期)
一旦确定 X 和 Y,对任意 b 的最终槽位可分两步计算:
- 若 b ≤ X:直接模拟 b 步;
- 若 b > X:先走 X 步到达 cycle 入口,再走 (b − X) % Y 步(跳过完整循环轮数)。
✅ 实现要点(单次查询 O(n))
使用哈希表记录每个槽位首次访问的步数,遍历时一旦遇到已见槽位,即可立即提取 X 和 Y:
def final_slot_optimized(a, b, slots):
n = len(slots)
if b == 0:
return a
# 记录每个槽位首次出现的步数
first_seen = {}
path = [] # 存储访问路径(槽位序列),索引即步数
current = a
# 模拟直到发现重复或走完 b 步
step = 0
while step <= b and current not in first_seen:
first_seen[current] = step
path.append(current)
current = (current + slots[current]) % n
step += 1
# 若未提前终止:b 很小,已得到答案
if step > b:
return path[b]
# 发现循环:first_seen[current] 是 cycle 入口步数 X
X = first_seen[current]
Y = step - X # 当前步数 - 入口步数 = cycle 长度
if b <= X:
return path[b]
else:
# 走 X 步到 cycle 入口,再走剩余偏移
offset = (b - X) % Y
return path[X + offset]⚠️ 注意事项与边界处理
- 模运算安全:slots[i] 可为负数,Python 中 (i + slots[i]) % n 自动处理负模(如 -1 % 5 == 4),无需额外修正。
- 索引一致性:题目未明确槽位编号是否从 0 或 1 开始。示例代码使用 0-based,若输入为 1-based(如 a=1),需统一减 1 处理。
- 空间优化:path 数组最坏存 n+1 项,空间复杂度 O(n),可接受;若内存极端受限,可在检测到 cycle 后仅缓存 cycle 区间 [X, step)。
- 多查询场景:若多个查询共享同一 slots 数组,可预计算所有起始点的循环信息(需 O(n²) 预处理),但通常 q 较小,对每个查询独立检测更简洁。
? 总结
本方案将单次查询时间复杂度从 O(b) 严格降为 O(n),彻底解决 b = 10^12 类超大输入瓶颈。核心在于利用有限状态机的必然循环性,通过一次正向遍历完成 intro/cycle 识别,再用模运算跳过冗余周期。该思想广泛适用于图上路径、迭代函数、链表环检测等场景,是算法优化中「找规律替代硬算」的经典范式。










