CC++ & Algorithm

倍增法——快速跨越,一步当两步

较难23
语言版本:C++
概述:倍增法就像跳格子时每次跳2的幂次步,用二进制思想快速到达目标,常用于快速幂和祖先查找。

倍增法:一步当两步,速度翻倍

你有没有想过,为什么计算机能快速算出 3133^{13} 这种很大的数?如果用乘法一个一个乘,要乘13次。但要是能“一次跳好几步”,比如先算出 313^1、再算出 323^2、再算出 343^4、再算出 383^8,然后只用其中几个乘起来(比如 38×34×31=3133^8 \times 3^4 \times 3^1 = 3^{13}),那就只做了几次乘法。这种“每次加倍”的方法,就叫倍增法

倍增法的核心思想是:把需要做的事情拆成若干个“2的幂次”来做,因为任何正整数都可以写成若干个2的幂次相加(也就是二进制分解)。这样,原本需要循环N次的操作,就变成了循环 log2N\log_2 N 次,速度大大提升。


生活中的跳格子——理解“二进制分解”

想象你站在一条数轴上的0点,想跳到13的位置。你每次可以跳1步、2步、4步、8步……(也就是2的幂次步)。你怎么用最少的跳跃次数到达13?

13的二进制是1101,表示 13=8+4+113 = 8 + 4 + 1。所以你只需要跳三次:先跳8步到8,再跳4步到12,最后跳1步到13。如果每次跳跃都只能跳固定步长(比如1步),你需要跳13次;而用2的幂次步长,只跳了3次。

倍增法就是利用这个道理:把一个大任务(比如计算大次方、找祖先、查区间最值)拆成若干个2的幂次小任务,然后“跳着”完成。


快速幂——最经典的倍增法应用

快速幂用来计算 xnx^n (n 很大时),比如求 313mod10000000073^{13} \bmod 1000000007。直接乘13次要循环13次,而快速幂只需要循环4次(因为13的二进制有4位)。

核心步骤

  1. 把指数 n 写成二进制,比如 n=13=11012n=13 = 1101_2
  2. 从低到高依次处理每一位:
    • 如果当前位是1,就把当前底数的值乘到结果中。
    • 每处理一位,底数就自乘一次(变成 x,x2,x4,x8,x, x^2, x^4, x^8, \dots)。
    • 指数右移一位(相当于去掉最低位)。

举个具体的数字例子

计算 3133^{13}(不取模,方便看出数值):

  • 初始:res = 1,x = 3,n = 13(二进制1101)
  • 第1步:n最低位=1,res = 1×3 = 3;x = 3×3 = 9;n右移一位变为6(二进制110)
  • 第2步:n最低位=0,不乘;x = 9×9 = 81;n右移一位变为3(二进制11)
  • 第3步:n最低位=1,res = 3×81 = 243;x = 81×81 = 6561;n右移一位变为1(二进制1)
  • 第4步:n最低位=1,res = 243×6561 = 1594323;x = 6561×6561;n右移一位变为0,结束

结果就是 313=15943233^{13} = 1594323,而实际 3133^{13} 确实等于1594323,正确。

代码实现(带中文注释)

#include <iostream>
using namespace std;

// 快速幂:计算 x^n % mod
long long quickPow(long long x, long long n, long long mod) {
    long long res = 1;          // 结果初始为1
    while (n > 0) {             // 当指数 n 不为0时循环
        if (n & 1) {            // 如果当前二进制最低位是1
            res = (res * x) % mod; // 乘上当前 x 并取模
        }
        x = (x * x) % mod;      // x 自乘变为 x^2,取模防止溢出
        n >>= 1;                // 指数右移一位(相当于除以2)
    }
    return res;
}

int main() {
    long long x = 3;            // 底数
    long long n = 13;           // 指数
    long long mod = 1000000007; // 模数(为了让结果不爆)
    cout << x << "的" << n << "次方模" << mod << " = " 
         << quickPow(x, n, mod) << endl;
    return 0;
}

运行结果:3的13次方模1000000007 = 1594323(因为3^13 = 1594323,小于模数,所以取模后不变)。


倍增法的其他应用(简单了解)

1. 树上最近公共祖先(LCA)

在一棵树中,要找到两个节点的最近公共祖先(比如你和你爷爷的最近公共祖先是你爸爸)。如果用普通方法,需要一层一层往上走,最坏情况要遍历整棵树。但我们可以预处理每个节点往上跳 20,21,22,,2k2^0, 2^1, 2^2, \dots, 2^k 步的祖先是谁。查询时,我们让两个节点先跳到同一深度,然后一起用二进制分解往上跳,直到他们的父节点相同。这样每次查询只花 logN\log N 时间。

2. 区间最值查询(ST表)

给你一个数组,要快速回答“区间[L, R]的最大值”。我们可以预处理出每个位置开始长度为 2k2^k 的区间最大值。查询时,把区间长度拆成两个2的幂次区间,取最大值。比如区间长度是13,就拆成长度为8和5(但5不是2的幂次?实际上ST表用两个长度都是2的幂次且重叠的区间覆盖整个查询区间,因为取最大值可以重复)。这也是倍增思想的应用。

3. 跳石头问题(倍增移动)

假设你从0出发,每次可以跳1步、2步、4步、8步……问能否恰好跳到某个目标点?答案就是看目标数的二进制表示中哪些位是1。这其实就是快速幂思想的几何版本。


新手容易犯的错误

错误1:忘记取模导致整数溢出

快速幂常用在取模场景,如果不取模,x自乘几次后很快就会超出 long long 范围。比如计算 2602^{60} 直接乘会溢出。所以一定要每一步都取模。

错误2:指数循环条件弄错

有些人写成 while (n >= 0),但n右移最终会变成0,如果n是0还会再进入循环,导致死循环或错误结果。正确条件是 while (n > 0)

错误3:误用 n&1 判断位

n & 1 只检查最低位,很多新手以为可以检查任意位,其实这是正确做法,因为每次循环都把最低位去掉(右移),所以每一轮检查的其实是最低位。但要注意括号:if (n & 1) 在C++中没问题,但为了清晰最好写成 if (n & 1) 可读,或者 if (n % 2 == 1)

错误4:变量类型用错

指数 n 如果是负数,快速幂不适用(需要特殊处理)。通常我们只处理非负整数指数。另外,xmod 确保是 long long 类型,避免乘法溢出。


完整可运行的示例(带多个测试)

下面是一个完整的程序,它演示了快速幂的几种情况,并对比了普通乘法的效率。

#include <iostream>
using namespace std;

// 快速幂:计算 x^n % mod
long long quickPow(long long x, long long n, long long mod) {
    long long res = 1;          // 结果初始化为1
    while (n > 0) {             // 直到指数为0
        if (n & 1) {            // 如果当前二进制位是1
            res = (res * x) % mod; // 乘上当前x
        }
        x = (x * x) % mod;      // x平方
        n >>= 1;                // n右移一位
    }
    return res;
}

int main() {
    // 测试1: 计算 2^10 % 1000
    cout << "2^10 % 1000 = " << quickPow(2, 10, 1000) << endl; // 输出24(因为1024%1000=24)

    // 测试2: 计算 5^3 % 100(普通乘法就是125%100=25)
    cout << "5^3 % 100 = " << quickPow(5, 3, 100) << endl;

    // 测试3: 大指数 3^1000000 % 1000000007
    cout << "3^1000000 % 1000000007 = " << quickPow(3, 1000000, 1000000007) << endl;

    // 测试4: 底数为0的特殊情况
    cout << "0^10 % 10 = " << quickPow(0, 10, 10) << endl; // 输出0

    // 测试5: 指数为0的情况
    cout << "7^0 % 10 = " << quickPow(7, 0, 10) << endl;   // 输出1

    return 0;
}

运行结果示例:

2^10 % 1000 = 24
5^3 % 100 = 25
3^1000000 % 1000000007 = 537390328
0^10 % 10 = 0
7^0 % 10 = 1

注意:最后的结果可能每次因为取模运算而不同,但都是正确的。


相关指引

  • 如果你学会了快速幂,可以进一步学习矩阵快速幂(用于递推数列加速,比如斐波那契数列)。
  • 倍增法在树上LCA(最近公共祖先)和ST表(区间最值)中也是核心思想,建议在学完树和图的基本概念后去了解。
  • 还有一种叫二分答案+倍增的技巧,常用于某些“先判断再跳”的问题(比如在数轴上找第一个满足条件的位置)。
  • 在CSP-J中,倍增法常和数组操作模拟数的二进制结合出题,可以用它来优化原本需要O(N)的循环到O(log N)。

一句话总结: 倍增法就是把任务“拆成2的幂次份”,每次加倍,跳过中间步骤,大幅提升速度。从快速幂开始,你会发现很多问题都能用这个“跳格子”的思路轻松解决!

例题精讲

1单选题

使用倍增法计算 a^b mod p 的时间复杂度是?

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

倍增法在求解最近公共祖先(LCA)问题时,预处理阶段的时间复杂度为 O(n log n)。

3填空题
以下代码使用倍增法计算 a^b mod p,请填空:
long long fast_pow(long long a, long long b, long long p) {
    long long res = 1;
    while (b) {
        if (b & 1) res = ___;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}
4单选题

在 ST 表(Sparse Table)中,使用倍增法预处理区间最大值时,数组 st[i][j] 表示从 i 开始长度为 2^j 的区间最大值,那么 st[i][0] 应初始化为?

Aa[i]
Ba[i+1]
Cmax(a[i], a[i+1])
D0
5判断题

倍增法只能用于处理2的幂次相关问题,不能处理任意幂次或跳跃。