目录
[一、什么是倍增思想?------ 从 "一步步走" 到 "跳着走"](#一、什么是倍增思想?—— 从 “一步步走” 到 “跳着走”)
[1.1 倍增思想的核心本质](#1.1 倍增思想的核心本质)
[1.2 倍增思想的数学基础](#1.2 倍增思想的数学基础)
[1.3 倍增思想的适用场景](#1.3 倍增思想的适用场景)
[二、倍增思想的入门实践:快速幂(O (log b) 求 a^b mod p)](#二、倍增思想的入门实践:快速幂(O (log b) 求 a^b mod p))
[2.1 问题引入:为什么需要快速幂?](#2.1 问题引入:为什么需要快速幂?)
[2.2 快速幂的原理:二进制分解 + 倍增计算](#2.2 快速幂的原理:二进制分解 + 倍增计算)
[2.3 快速幂的 C++ 实现(递归 + 迭代版)](#2.3 快速幂的 C++ 实现(递归 + 迭代版))
[2.3.1 递归版快速幂](#2.3.1 递归版快速幂)
[2.3.2 迭代版快速幂(推荐)](#2.3.2 迭代版快速幂(推荐))
[2.4 快速幂的边界情况处理](#2.4 快速幂的边界情况处理)
[三、倍增思想的延伸:大整数乘法取模(a×b mod p)](#三、倍增思想的延伸:大整数乘法取模(a×b mod p))
[3.1 问题引入:为什么需要大整数乘法取模?](#3.1 问题引入:为什么需要大整数乘法取模?)
[3.2 大整数乘法取模的原理:倍增 + 加法取模](#3.2 大整数乘法取模的原理:倍增 + 加法取模)
[3.3 大整数乘法取模的 C++ 实现(快速乘)](#3.3 大整数乘法取模的 C++ 实现(快速乘))
[3.4 快速幂 + 快速乘:解决超大数幂运算取模](#3.4 快速幂 + 快速乘:解决超大数幂运算取模)
前言
在算法世界中,有一类思想如同 "四两拨千斤" 的智慧 ------ 它通过将问题规模 "翻倍增长" 的方式,把原本复杂的运算转化为高效的分步处理,这就是倍增思想。从基础的快速幂运算,到超大数乘法取模,再到后续可能延伸的 LCA(最近公共祖先)、RMQ(区间最值查询)等高级应用,倍增思想始终以 "高效、简洁" 的核心优势,成为程序员必须掌握的核心算法思想之一。
本文将从倍增思想的本质出发,结合数学原理、具体案例和 C++ 实战代码,由浅入深地拆解倍增思想的应用场景。无论你是算法入门的新手,还是寻求进阶优化的开发者,都能通过本文彻底理解倍增思想的底层逻辑,并灵活运用到实际编程中。下面就让我们正式开始吧!
一、什么是倍增思想?------ 从 "一步步走" 到 "跳着走"
1.1 倍增思想的核心本质
倍增,顾名思义,就是 "每次增加一倍"。它的核心思想是:通过预先计算出问题的 "2^k 倍" 解,在实际求解时,将复杂问题分解为若干个 "2^k 倍" 的子问题,从而快速合并得到最终答案。
举个生活中的例子:如果我们要从 1 数到 1000,最朴素的方法是逐个累加(1→2→3→...→1000),需要 1000 次操作;而用倍增思想,我们可以先数到 2(1×2)、再到 4(2×2)、8(4×2)、...、512(2^9),再通过组合这些 "倍增步长"(512+256+128+64+32+8),仅需少数几次操作就能到达 1000。
在算法中,倍增思想的核心价值在于将时间复杂度从 O (n) 优化到 O (log n) 。这种优化在处理大数据(如 10^18 级别)时,几乎是不可或缺的 ------ 毕竟,O (n) 的算法在 n=10^18 时完全无法运行,而 O (log n) 仅需 60 次左右操作就能完成。
1.2 倍增思想的数学基础
倍增思想的数学支撑是二进制分解。任何一个正整数 n,都可以唯一分解为若干个不重复的 2 的幂次之和。例如:
- 11 = 8 + 2 + 1 = 2³ + 2¹ + 2⁰
- 100 = 64 + 32 + 4 = 2⁶ + 2⁵ + 2²
- 1000 = 512 + 256 + 128 + 64 + 32 + 8 = 2⁹ + 2⁸ + 2⁷ + 2⁶ + 2⁵ + 2³
这种分解的优势在于:每个 "2^k" 项都是前一项的 2 倍,我们可以通过 "自乘" 快速计算出所有需要的 "2^k" 项,再根据目标数的二进制表示,选择性地将这些项组合起来,得到最终结果。
1.3 倍增思想的适用场景
倍增思想并非万能,但在以下场景中能发挥巨大作用:
- 幂运算 / 乘法运算:如快速幂(a^b mod p)、大整数乘法取模(a×b mod p,a、b 达 10^18 级别);
- 区间查询:如 RMQ(区间最值查询)、区间和查询等,通过预处理倍增数组实现 O (1) 或 O (log n) 查询;
- 树形结构:如 LCA(最近公共祖先),通过倍增预处理父节点信息,快速找到两个节点的公共祖先;
- 字符串处理:如 KMP 算法的优化、后缀数组的构建等;
- 其他大数据场景:如寻找一个数的约数、求解递推数列的第 n 项等。
本文将重点讲解倍增思想在快速幂 和大整数乘法取模中的应用(这是最基础也是最常用的场景),后续会延伸到其他高级应用。
二、倍增思想的入门实践:快速幂(O (log b) 求 a^b mod p)
2.1 问题引入:为什么需要快速幂?
在编程中,我们经常遇到 "计算 a 的 b 次幂对 p 取模" 的需求(例如密码学、数论问题、组合数学等场景)。如果直接使用朴素算法:
cpp
// 朴素幂运算:O(b)时间复杂度
long long pow_naive(long long a, long long b, long long p) {
long long res = 1;
for (int i = 0; i < b; i++) {
res = res * a % p;
}
return res;
}
当 b 的取值较大(如 b=10^18)时,这个算法会直接超时 ------ 因为 10^18 次循环是计算机无法在合理时间内完成的。
而快速幂算法通过倍增思想,将时间复杂度优化到 O (log b),即使 b=10^18,也仅需 60 次左右循环就能完成计算,效率提升巨大。
2.2 快速幂的原理:二进制分解 + 倍增计算
快速幂的核心思路的是将 b 分解为二进制,再通过倍增计算 a 的 2^k 次幂,最后将对应位为 1 的项相乘。
以计算 3^11 mod 9 为例,步骤如下:
- 分解 b=11 的二进制:11 = 8 + 2 + 1 = 2³ + 2¹ + 2⁰,二进制表示为 1011;
- 倍增计算 a 的 2^k 次幂:
- 2⁰次幂:3^1 = 3
- 2¹ 次幂:3^2 = (3^1) × (3^1) = 9
- 2² 次幂:3^4 = (3^2) × (3^2) = 81
- 2³ 次幂:3^8 = (3^4) × (3^4) = 6561
- 筛选二进制中为 1 的项相乘:3^11 = 3^8 × 3^2 × 3^1;
- 每一步相乘后取模(避免溢出):(6561 × 9 × 3) mod 9 = (6561 mod 9) × (9 mod 9) × (3 mod 9) mod 9 = 0 × 0 × 3 mod 9 = 0?不对,重新计算:实际计算时,3^2=9 mod9=0,所以 3^11=3^8 × 3^2 × 3^1 = (3^8 mod9) × (0) × (3 mod9) = 0,结果正确。
再举一个示例:计算 2^10 mod9:
- 分解 b=10 的二进制:10=8+2=2³+2¹,二进制为 1010;
- 倍增计算:
- 2^0 次幂:2^1=2
- 2^1 次幂:2^2=4
- 2^2 次幂:2^4=16
- 2^3 次幂:2^8=256
- 相乘取模:(256 ×4) mod9 = 1024 mod9。因为 9×113=1017,1024-1017=7,所以结果为 7,与示例输出一致。
2.3 快速幂的 C++ 实现(递归 + 迭代版)
快速幂有两种常见实现方式:递归版(思路清晰)和迭代版(效率更高,无栈溢出风险)。
2.3.1 递归版快速幂
递归版的核心是 "分而治之":将 a^b 分解为 (a^(b/2))²,如果 b 是奇数,则再乘以 a。
cpp
#include <iostream>
using namespace std;
typedef long long LL;
// 递归版快速幂:计算 (a^b) mod p
LL quick_pow_recursive(LL a, LL b, LL p) {
// 递归终止条件:b=0时,任何数的0次幂都是1
if (b == 0) return 1 % p;
// 递归计算 a^(b/2) mod p
LL mid = quick_pow_recursive(a, b >> 1, p);
// 合并结果:(a^(b/2))^2 mod p
LL res = mid * mid % p;
// 如果b是奇数,需要再乘以a mod p
if (b & 1) res = res * a % p;
return res;
}
int main() {
LL a, b, p;
cin >> a >> b >> p;
LL result = quick_pow_recursive(a, b, p);
printf("%lld^%lld mod %lld=%lld\n", a, b, p, result);
return 0;
}
优点:代码简洁,思路和二进制分解的逻辑高度一致,容易理解;
缺点:当 b 较大时(如 10^18),递归深度会达到 log2 (10^18)≈60,虽然不会栈溢出,但递归调用的开销比迭代版略大。
2.3.2 迭代版快速幂(推荐)
迭代版通过循环遍历 b 的二进制位,直接计算倍增项并累加结果,效率更高,是实际编程中最常用的版本。
cpp
#include <iostream>
using namespace std;
typedef long long LL;
// 迭代版快速幂:计算 (a^b) mod p
LL quick_pow_iterative(LL a, LL b, LL p) {
LL res = 1 % p; // 初始化结果为1 mod p(处理p=1的特殊情况)
a = a % p; // 先将a对p取模,减少后续计算量
while (b > 0) {
// 如果当前b的最低位是1,将当前a乘到结果中
if (b & 1) {
res = res * a % p;
}
// 倍增a:a = a^2 mod p(对应下一个二进制位)
a = a * a % p;
// b右移一位,处理下一个二进制位
b >>= 1;
}
return res;
}
int main() {
LL a, b, p;
cin >> a >> b >> p;
LL result = quick_pow_iterative(a, b, p);
printf("%lld^%lld mod %lld=%lld\n", a, b, p, result);
return 0;
}
代码解析:
- 初始化 res=1% p:处理 p=1 的特殊情况(任何数 mod1 都是 0);
- a=a% p:提前对 a 取模,避免 a 过大导致的溢出(尤其是当 a 本身大于 p 时);
- 循环遍历 b 的二进制位:
- b&1:判断当前 b 的最低位是否为 1(如果是,说明需要将当前的 a 乘到结果中);
- a = a*a%p:倍增 a,从 a^1→a^2→a^4→a^8→...;
- b>>=1:b 右移一位,相当于除以 2,处理下一个二进制位。
测试用例:输入:2 10 9输出:2^10 mod 9=7与示例结果一致,证明代码正确性。
2.4 快速幂的边界情况处理
在实际使用中,需要注意以下边界情况,避免代码出错:
- p=1:任何数 mod1 都是 0,直接返回 0;
- a=0:当 b=0 时,0^0 无意义(通常题目会避免这种情况),当 b>0 时,0^b mod p=0;
- b=0:任何非零数的 0 次幂都是 1,所以返回 1 mod p;
- 数据溢出 :由于 a 和 b 可能很大(如 10^18),直接相乘会导致 64 位整数溢出。解决方案:
- 提前对 a 取模(a=a% p);
- 使用 "快速乘" 优化(下文会讲),将乘法转化为加法,避免溢出。
相关的题目链接如下:https://www.luogu.com.cn/problem/P1226
三、倍增思想的延伸:大整数乘法取模(a×b mod p)
3.1 问题引入:为什么需要大整数乘法取模?
当 a 和 b 的取值范围达到 10^18 级别时,即使使用 64 位整数(long long),直接计算 a×b 也会发生溢出 ------ 因为 10^18 ×10^18=10^36,远超 long long 的最大值(约 9×10^18)。
此时,我们需要一种高效的方法来计算 (a×b) mod p,而无需直接计算 a×b 的完整结果。这就是**"大整数乘法取模"** 问题,其解决方案同样基于倍增思想。
3.2 大整数乘法取模的原理:倍增 + 加法取模
大整数乘法取模的核心思路是将乘法转化为加法,通过倍增累加的方式计算结果,本质上和快速幂的逻辑一致:
- 将 b 分解为二进制:b = 2^k1 + 2^k2 + ... + 2^km;
- 倍增计算 a×2^k mod p(即 a、2a、4a、8a、... mod p);
- 将对应二进制位为 1 的项累加,每次累加后取模,得到最终结果。
以计算 3×11 mod9 为例:
- 分解 b=11 的二进制:11=8+2+1=2³+2¹+2⁰;
- 倍增计算 a×2^k mod9:
- 2^0:3×1=3 mod9=3
- 2^1:3×2=6 mod9=6
- 2^2:3×4=12 mod9=3
- 2^3:3×8=24 mod9=6
- 累加对应项:3+6+6=15 mod9=6,结果正确(3×11=33 mod9=6)。
3.3 大整数乘法取模的 C++ 实现(快速乘)
这种方法也被称为**"快速乘"(Quick Multiplication)**,其迭代实现如下:
cpp
#include <iostream>
using namespace std;
typedef long long LL;
// 快速乘:计算 (a * b) mod p,避免溢出
LL quick_mul(LL a, LL b, LL p) {
LL res = 0 % p; // 初始化结果为0(加法累加的初始值)
a = a % p; // 提前对a取模,减少计算量
while (b > 0) {
// 如果当前b的最低位是1,将当前a加到结果中
if (b & 1) {
res = (res + a) % p;
}
// 倍增a:a = a*2 mod p(对应下一个二进制位)
a = (a + a) % p;
// b右移一位,处理下一个二进制位
b >>= 1;
}
return res;
}
int main() {
LL a, b, p;
cin >> a >> b >> p;
LL result = quick_mul(a, b, p);
printf("%lld * %lld mod %lld=%lld\n", a, b, p, result);
return 0;
}
代码解析:
- 初始化 res=0% p:加法累加的初始值,处理 p=1 的特殊情况;
- a=a% p:提前对 a 取模,避免 a 过大导致的溢出;
- 循环遍历 b 的二进制位:
b&1:判断当前 b 的最低位是否为 1(如果是,将当前的 a 加到结果中);a = (a+a)%p:倍增 a,从 a→2a→4a→8a→...(本质是 a×2^k);b>>=1:b 右移一位,处理下一个二进制位。
测试用例:
输入:2 3 9
输出:2*3 mod9=6(正确);
输入:10^18 10^18 1e9+7(假设输入为 1000000000000000000 1000000000000000000 1000000007),代码能正常计算结果,无溢出。
3.4 快速幂 + 快速乘:解决超大数幂运算取模
当 a 和 b 都达到 10^18 级别时,快速幂中的res * a % p也会发生溢出。此时,我们可以用快速乘替代普通乘法,实现 "双重倍增",彻底避免溢出。
组合后的代码如下:
cpp
#include <iostream>
using namespace std;
typedef long long LL;
// 快速乘:计算 (a * b) mod p,避免溢出
LL quick_mul(LL a, LL b, LL p) {
LL res = 0 % p;
a = a % p;
while (b > 0) {
if (b & 1) {
res = (res + a) % p;
}
a = (a + a) % p;
b >>= 1;
}
return res;
}
// 快速幂(结合快速乘):计算 (a^b) mod p,支持超大数
LL quick_pow(LL a, LL b, LL p) {
LL res = 1 % p;
a = a % p;
while (b > 0) {
if (b & 1) {
res = quick_mul(res, a, p); // 用快速乘替代普通乘法
}
a = quick_mul(a, a, p); // 用快速乘替代普通乘法
b >>= 1;
}
return res;
}
int main() {
// 测试:计算 (1e18 ^ 1e18) mod (1e9+7)
LL a = 1e18, b = 1e18, p = 1e9 + 7;
LL result = quick_pow(a, b, p);
printf("%lld^%lld mod %lld=%lld\n", a, b, p, result);
return 0;
}
核心改进 :将快速幂中的res * a % p和a * a % p替换为quick_mul(res, a, p)和quick_mul(a, a, p),彻底解决了超大数乘法的溢出问题。
相关的题目链接如下:https://www.luogu.com.cn/problem/P10446
总结
倍增思想是算法优化中的 "利器",它看似简单,却能解决一系列大数据场景下的复杂问题。从快速幂到快速乘,再到 RMQ、LCA 等高级应用,倍增思想的核心逻辑始终如一 ------ 通过 "跳着走" 的方式,将复杂问题分解为高效的子问题。
本文通过数学原理、案例解析和 C++ 代码实现,详细讲解了倍增思想的基础应用。希望你在学习后,能够熟练掌握快速幂和快速乘的模板,并尝试将倍增思想运用到更多场景中。算法学习的核心是 "理解思想,灵活运用",而不是死记硬背模板。只有真正理解了倍增思想的底层逻辑,才能在面对不同问题时,快速想到对应的优化方案。
最后,建议你多做实战题目,通过练习巩固所学知识。如果在学习过程中遇到问题,可以回头再看本文的思路解析,或者查阅相关题解,不断加深对倍增思想的理解。祝你在算法的道路上越走越远!