倍增法——快速跨越,一步当两步
较难23倍增法:一步当两步,速度翻倍
你有没有想过,为什么计算机能快速算出 这种很大的数?如果用乘法一个一个乘,要乘13次。但要是能“一次跳好几步”,比如先算出 、再算出 、再算出 、再算出 ,然后只用其中几个乘起来(比如 ),那就只做了几次乘法。这种“每次加倍”的方法,就叫倍增法。
倍增法的核心思想是:把需要做的事情拆成若干个“2的幂次”来做,因为任何正整数都可以写成若干个2的幂次相加(也就是二进制分解)。这样,原本需要循环N次的操作,就变成了循环 次,速度大大提升。
生活中的跳格子——理解“二进制分解”
想象你站在一条数轴上的0点,想跳到13的位置。你每次可以跳1步、2步、4步、8步……(也就是2的幂次步)。你怎么用最少的跳跃次数到达13?
13的二进制是1101,表示 。所以你只需要跳三次:先跳8步到8,再跳4步到12,最后跳1步到13。如果每次跳跃都只能跳固定步长(比如1步),你需要跳13次;而用2的幂次步长,只跳了3次。
倍增法就是利用这个道理:把一个大任务(比如计算大次方、找祖先、查区间最值)拆成若干个2的幂次小任务,然后“跳着”完成。
快速幂——最经典的倍增法应用
快速幂用来计算 (n 很大时),比如求 。直接乘13次要循环13次,而快速幂只需要循环4次(因为13的二进制有4位)。
核心步骤
- 把指数 n 写成二进制,比如 。
- 从低到高依次处理每一位:
- 如果当前位是1,就把当前底数的值乘到结果中。
- 每处理一位,底数就自乘一次(变成 )。
- 指数右移一位(相当于去掉最低位)。
举个具体的数字例子
计算 (不取模,方便看出数值):
- 初始: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,结束
结果就是 ,而实际 确实等于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)
在一棵树中,要找到两个节点的最近公共祖先(比如你和你爷爷的最近公共祖先是你爸爸)。如果用普通方法,需要一层一层往上走,最坏情况要遍历整棵树。但我们可以预处理每个节点往上跳 步的祖先是谁。查询时,我们让两个节点先跳到同一深度,然后一起用二进制分解往上跳,直到他们的父节点相同。这样每次查询只花 时间。
2. 区间最值查询(ST表)
给你一个数组,要快速回答“区间[L, R]的最大值”。我们可以预处理出每个位置开始长度为 的区间最大值。查询时,把区间长度拆成两个2的幂次区间,取最大值。比如区间长度是13,就拆成长度为8和5(但5不是2的幂次?实际上ST表用两个长度都是2的幂次且重叠的区间覆盖整个查询区间,因为取最大值可以重复)。这也是倍增思想的应用。
3. 跳石头问题(倍增移动)
假设你从0出发,每次可以跳1步、2步、4步、8步……问能否恰好跳到某个目标点?答案就是看目标数的二进制表示中哪些位是1。这其实就是快速幂思想的几何版本。
新手容易犯的错误
错误1:忘记取模导致整数溢出
快速幂常用在取模场景,如果不取模,x自乘几次后很快就会超出 long long 范围。比如计算 直接乘会溢出。所以一定要每一步都取模。
错误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 如果是负数,快速幂不适用(需要特殊处理)。通常我们只处理非负整数指数。另外,x 和 mod 确保是 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的幂次份”,每次加倍,跳过中间步骤,大幅提升速度。从快速幂开始,你会发现很多问题都能用这个“跳格子”的思路轻松解决!
例题精讲
使用倍增法计算 a^b mod p 的时间复杂度是?
倍增法在求解最近公共祖先(LCA)问题时,预处理阶段的时间复杂度为 O(n log n)。
以下代码使用倍增法计算 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;
}在 ST 表(Sparse Table)中,使用倍增法预处理区间最大值时,数组 st[i][j] 表示从 i 开始长度为 2^j 的区间最大值,那么 st[i][0] 应初始化为?
倍增法只能用于处理2的幂次相关问题,不能处理任意幂次或跳跃。