CC++ & Algorithm

倍增法思想:像魔法跳跃一样快速到达终点

困难4
语言版本:C++Python
概述:本文用“翻倍跳跃”的比喻,讲解倍增法预处理和查询的思想,并给出Python代码实现。

倍增法:用翻倍跳跃快速到达终点

小朋友们,你们有没有想过,如果要从起点走到终点,每次只能走一步,那要走很久很久。但如果有一种神奇的魔法,可以让你一次走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 变成 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拆成二进制,每次平方底数,是另一种“翻倍”思想。

学会倍增法,你就掌握了一种“用空间换时间”的经典算法,以后遇到需要“跳步”的问题,就能像魔法师一样瞬间到达终点啦!

例题精讲

1单选题

在倍增法中,预处理时通常需要计算每个位置跳2^k步到达的位置。对于一个长度为N的数组,预处理的时间复杂度是多少?

AO(N)
BO(N log N)
CO(N^2)
DO(log N)
2判断题

倍增法思想可以用于求解静态数组的区间最值查询(RMQ)问题,预处理后每次查询能在O(1)时间内完成。

3填空题
下面是使用倍增思想实现快速幂(计算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
4单选题

关于倍增法的描述,以下哪个选项是正确的?

A倍增法允许从起点一次跳任意2的幂次步数,并能解决所有跳跃问题
B倍增法预处理每个点跳2^k步后的位置,但每次只能选择一种步长跳跃,不能组合
C跳台阶问题(每次1或2级)是倍增法的典型应用
D倍增法只能用于静态数组,不能用于树结构
5判断题

在最近公共祖先(LCA)问题中,使用倍增法预处理每个节点的第2^k个祖先,可以在O(log N)时间内查询任意两个节点的LCA。