倍增法思想:像魔法跳跃一样快速到达终点
困难4倍增法:用翻倍跳跃快速到达终点
小朋友们,你们有没有想过,如果要从起点走到终点,每次只能走一步,那要走很久很久。但如果有一种神奇的魔法,可以让你一次走2步、4步、8步……那就能飞快地到达啦!这就是倍增法的核心思想:通过提前计算好每次翻倍跳跃能到达的位置,然后在需要的时候用最少的跳跃次数到达目标。
倍增法特别适合解决“从一个位置出发,走任意步数后到了哪里”这类问题。比如你在操场上有一排格子,从0号格子出发,想知道走18步后到哪个格子。用倍增法,我们预先算出从每个格子走1步、2步、4步、8步……能到的格子,然后把18拆成16+2,先跳16步,再跳2步,两次就搞定,不需要一步一步数18次!
下面我们分几个部分来学习这个神奇的“魔法跳跃”。
1. 什么是倍增法?——用翻倍代替一步一步走
想象你有一排连续的位置,编号从0到N。你想从位置i出发,走step步。普通方法是一个一个走,需要step次操作。倍增法提前准备一张“跳跃表”,表中记录了从每个位置一次跳1步、2步、4步、8步……(即2^0、2^1、2^2、2^3……步)能到的位置。
例如,你手里有一张地图,上面写着:
- 从位置0走1步到位置1,走2步到位置2,走4步到位置4,走8步到位置8……
- 从位置1走1步到位置2,走2步到位置3,走4步到位置5,走8步到位置9……
当你需要从位置0走13步时,可以查地图:13 = 8 + 4 + 1,于是先跳8步到8,再跳4步到12,再跳1步到13。只用3次跳跃,比13次快多了。
生活中的例子:你有一张“步长卡”,上面记录了你每跳一次能走多远:第一次跳1米,第二次跳2米,第三次跳4米……每次翻倍。如果要去18米远的地方,你不需要一步一步数,而是先跳16米(第5次跳),再跳2米(第2次跳),总共只跳2次。这就是倍增法的“翻倍跳跃”思想。
2. 如何构建跳跃表?——递推公式 up[i][k] = up[ up[i][k-1] ][k-1]
我们用二维数组 up[i][k] 表示从位置 i 出发,走 2^k 步后的位置。比如:
up[i][0]表示走 1 步(2^0)后的位置,即 i+1。up[i][1]表示走 2 步(2^1)后的位置,即先走1步到 i+1,再走1步到 i+2。up[i][2]表示走 4 步(2^2)后的位置,即先走2步到 up[i][1],再走2步到 up[ up[i][1] ][1]。
注意规律:走 2^k 步 = 先走 2^(k-1) 步,再走 2^(k-1) 步。于是得到递推公式:
up[i][k] = up[ up[i][k-1] ][k-1]
只要先算出所有 up[i][0],就能一层一层算出更大的 k 值。
举个例子:假设位置编号 0~9,走一步到下一个位置。那么:
up[0][0] = 1,up[1][0] = 2, …,up[8][0] = 9,up[9][0] = -1(越界了,用-1表示)- 计算
up[0][1]:先走1步到1,再走1步到2,所以up[0][1] = 2 - 计算
up[0][2]:先走2步到2,再走2步到4,所以up[0][2] = 4 - 计算
up[0][3]:先走4步到4,再走4步到8,所以up[0][3] = 8 - 计算
up[0][4]:先走8步到8,再走8步到16,但16>9,越界,所以up[0][4] = -1
这样我们就有了从0出发的各种跳跃能力。
3. 如何查询任意步数?——二进制拆分法
现在我们要从位置 pos 出发走 step 步。我们可以把 step 拆成若干个 2 的幂次之和。比如 step = 13 = 8 + 4 + 1(二进制 1101)。我们依次跳对应的 2^k 步:先跳 1 步(2^0),再跳 4 步(2^2),再跳 8 步(2^3)——注意顺序可以从大到小也可以从小到大,但为了代码方便,我们通常从低位到高位(或者高位到低位,结果一样)。代码里常用的是 依次判断 step 的二进制每一位,如果某一位是1,就跳对应的 2^k 步。
具体做法:
- 设定一个变量 k = 0,表示当前检查的 2 的幂次(从 2^0 开始)。
- 只要 step > 0:
- 如果 step 的最低位是 1(即 step & 1 == 1),就从当前位置跳 2^k 步:
pos = up[pos][k]。 - 然后把 step 右移一位(去掉最低位),k 加 1,准备检查下一个更高的位。
- 如果 step 的最低位是 1(即 step & 1 == 1),就从当前位置跳 2^k 步:
这样循环直到 step 变成 0,就到达了最终位置。
举例:从位置0走13步。
- step=13(二进制1101),k=0:最低位1,跳2^0=1步,pos=up[0][0]=1,step右移变成6(110),k=1
- step=6(110),k=1:最低位0,不跳,step右移变成3(11),k=2
- step=3(11),k=2:最低位1,跳2^2=4步,pos=up[1][2]=?需要查表:up[1][2] 是走4步,1→5,所以pos=5,step右移变成1(1),k=3
- step=1(1),k=3:最低位1,跳2^3=8步,pos=up[5][3]=?查表:up[5][3] 是走8步,5→13,但位置只有0-9,所以越界返回-1。最终pos=-1,表示超出范围。
如果步数较小,不越界,就能正确到达。
4. 生活中的例子:用零花钱买零食
假设你每天存一定数量的零花钱,第一天存1元,第二天存2元,第三天存4元……(每天翻倍)。你想知道第几天后总存款能买一个18元的玩具。用倍增法,你只需算:1+2+4+8+... 当存到16元时(第5天),还差2元,再存2元(第2天?不对,是再存一次2元?其实这里只是相似)。更贴切的例子是:你从家出发去学校,路上有公交站,每站间隔相等。你有一张“超级公交卡”,可以一次坐1站、2站、4站、8站……你想坐18站,用卡刷两次:先刷一个“8站卡”,再刷一个“2站卡”(注意顺序不重要)。这就像二进制拆分一样。
5. 常见错误与注意事项
错误1:数组越界
当跳到的位置超出范围时,需要特别处理。通常将越界标记为 -1,并且在查询时一旦遇到 -1 就立即返回 -1,表示无法到达。
错误2:忘记更新步数对应的 k
在查询循环中,每次检查完 step 的最低位后,必须右移 step 并且 k 加 1。如果忘记加 k,就会一直跳同样的 2^0 步,导致错误。
错误3:预处理的层数不够
比如位置最大为 N,那么只需要预处理到 k 满足 2^k > N 即可。通常取 MAX_LOG = ceil(log2(N+1)) 或直接用 while (1<<MAX_LOG) <= N: MAX_LOG+=1。层数太少会导致某些跳跃无法表示。
错误4:递推时跳到了 -1
如果 up[i][k-1] 已经是 -1,那么 up[i][k] 也应该为 -1。代码中要判断 if up[i][k-1] != -1 才继续,否则赋值为 -1。
6. 完整可运行的代码示例
下面是一个完整的 Python 程序,构建倍增表,并演示从位置0走不同步数的查询。代码里每一行都写了中文注释,方便理解。
# 倍增法示例:从位置0开始,走step步后的位置
N = 10 # 总位置数(0到9)
MAX_LOG = 4 # 因为2^4=16 > 10,所以4层足够
# 初始化up表,默认-1表示越界
up = [[-1] * MAX_LOG for _ in range(N)]
# 第一步:填充up[i][0](走1步)
for i in range(N):
up[i][0] = i + 1 # 走1步到下一个位置
if up[i][0] >= N: # 如果超出范围,设为-1
up[i][0] = -1
# 递推计算走2^k步(k从1到MAX_LOG-1)
for k in range(1, MAX_LOG):
for i in range(N):
if up[i][k-1] != -1: # 如果中间位置有效
up[i][k] = up[ up[i][k-1] ][k-1] # 先走2^(k-1)步,再走2^(k-1)步
else:
up[i][k] = -1 # 无效则标记越界
# 查询函数:从pos出发走step步
def jump(pos, step):
k = 0 # 当前二进制位对应的2^k
while step > 0:
if step & 1: # 如果step的最低位是1,需要跳
pos = up[pos][k] # 跳2^k步
if pos == -1: # 跳越界了,直接返回-1
return -1
step >>= 1 # step右移一位,去掉最低位
k += 1 # k加1,准备处理下一位
return pos
# 测试不同的步数
print("从位置0走5步后的位置:", jump(0, 5)) # 输出5
print("从位置0走13步后的位置:", jump(0, 13)) # 越界,输出-1
print("从位置0走0步后的位置:", jump(0, 0)) # 输出0(走0步即原地)
print("从位置0走10步后的位置:", jump(0, 10)) # 输出10,但位置只有0-9,所以越界返回-1?等一下,位置10不存在,所以越界,输出-1)
运行结果:
从位置0走5步后的位置: 5
从位置0走13步后的位置: -1
从位置0走0步后的位置: 0
从位置0走10步后的位置: -1
注意:因为位置只有0~9,所以走10步会到位置10,超过了N-1=9,所以返回-1。如果你想要位置10存在,可以把N设大一些。
7. 升级应用:从任意位置出发
上面的例子只从位置0出发。但倍增表已经记录了所有起始位置的信息,所以我们可以从任意位置出发查询。比如从位置3走6步(6=4+2):
print("从位置3走6步后的位置:", jump(3, 6)) # 3+6=9,在范围内,输出9
print("从位置5走8步后的位置:", jump(5, 8)) # 5+8=13,越界,输出-1
8. 相关知识点指引
- 二进制拆分:倍增法的基础,把任意整数拆成若干个2的幂之和。了解二进制表示和位运算(&、>>)很有帮助。
- 动态规划:倍增表的递推公式
up[i][k] = up[up[i][k-1]][k-1]其实是一种动态规划,用已知的小问题结果解决大问题。 - 树上倍增(LCA):倍增法在树上的重要应用,用于快速求两个节点的最近公共祖先。原理类似,先预处理每个节点向上跳2^k步的祖先。
- RMQ与ST表:区间最值查询也可以用倍增思想,预处理区间长度为2^k的最值,然后快速回答任意区间的最值。
- 快速幂:倍增法也可以用来快速计算 a^b,把b拆成二进制,每次平方底数,是另一种“翻倍”思想。
学会倍增法,你就掌握了一种“用空间换时间”的经典算法,以后遇到需要“跳步”的问题,就能像魔法师一样瞬间到达终点啦!
例题精讲
在倍增法中,预处理时通常需要计算每个位置跳2^k步到达的位置。对于一个长度为N的数组,预处理的时间复杂度是多少?
倍增法思想可以用于求解静态数组的区间最值查询(RMQ)问题,预处理后每次查询能在O(1)时间内完成。
下面是使用倍增思想实现快速幂(计算a^b mod m)的Python代码,请补全空缺部分。
def quick_pow(a, b, m):
result = 1
while b > 0:
if b & 1:
result = result * a % m
a = a * a % m
___
return result关于倍增法的描述,以下哪个选项是正确的?
在最近公共祖先(LCA)问题中,使用倍增法预处理每个节点的第2^k个祖先,可以在O(log N)时间内查询任意两个节点的LCA。