1. 快速幂算法概述
快速幂算法(Fast Exponentiation)是计算大数幂运算的高效方法,在信息学奥林匹克竞赛(CSP-S/NOIP提高组)中属于必须掌握的数学工具。传统幂运算的时间复杂度为O(n),而快速幂通过二分思想将其优化至O(log n),这在处理1e9+7这类大数取模运算时尤为关键。
我在2018年省赛中就遇到过一道必须使用快速幂的题目:给定a=2, b=1e18,要求计算a^b mod 1e9+7。当时用暴力解法直接TLE(时间超过限制),改用快速幂后仅用0.01秒就通过了测试。这个经历让我深刻认识到算法选择对竞赛成绩的决定性影响。
2. 快速幂核心原理拆解
2.1 二分思想与幂次分解
快速幂的核心在于将指数进行二进制分解。以计算3^13为例:
- 13的二进制表示为1101
- 因此3^13 = 3^(8+4+1) = 3^8 × 3^4 × 3^1
这种分解方式使得我们只需要计算log2(n)次乘法即可得到结果。具体实现时,我们通过不断平方底数和右移指数来实现:
cpp复制3^1 = 3
3^2 = (3^1)^2 = 9
3^4 = (3^2)^2 = 81
3^8 = (3^4)^2 = 6561
2.2 模运算性质应用
竞赛题目通常要求对结果取模(如mod 1e9+7),快速幂与模运算完美结合得益于以下性质:
- (a × b) mod m = [(a mod m) × (b mod m)] mod m
- 这意味着我们可以在每次乘法后立即取模,避免数值溢出
重要提示:在C++中,当模数为1e9+7时,必须使用long long类型存储中间结果,因为两个1e9级别的数相乘会超过int的存储范围(2^31-1≈2.1e9)
3. 标准实现与优化技巧
3.1 递归版实现
cpp复制typedef long long ll;
ll fastPow(ll a, ll b, ll mod) {
if (b == 0) return 1 % mod;
ll half = fastPow(a, b/2, mod);
if (b % 2 == 0) return half * half % mod;
return half * half % mod * a % mod;
}
递归版本虽然直观,但在竞赛中不推荐使用,因为:
- 递归调用栈有额外时间开销
- 可能引发栈溢出(虽然NOIP中栈空间通常足够)
3.2 迭代版实现(竞赛推荐)
cpp复制ll fastPow(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
这个版本是竞赛中的黄金标准,需要注意:
- 初始res必须设为1(乘法单位元)
b & 1判断当前二进制位是否为1- 每次循环a自乘相当于计算a^(2^i)
3.3 编译期优化技巧
对于需要多次调用的固定模数情况(如1e9+7),可以预先计算模数:
cpp复制const int MOD = 1e9+7;
inline ll fastPow(ll a, ll b) {
ll res = 1;
while (b) {
if (b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
使用inline关键字可以避免函数调用开销(虽然现代编译器通常会自动内联)
4. 竞赛中的典型应用场景
4.1 组合数计算
计算C(n,k) mod p时,根据公式:
C(n,k) = n! / (k!(n-k)!)
需要使用快速幂求分母的模逆元:
cpp复制ll comb(ll n, ll k, ll p) {
if (k > n) return 0;
ll res = 1;
// 计算分子 n!/(n-k)! = n×(n-1)×...×(n-k+1)
for (int i = 1; i <= k; ++i)
res = res * (n - k + i) % p;
// 乘以分母k!的逆元
ll den = 1;
for (int i = 1; i <= k; ++i)
den = den * i % p;
res = res * fastPow(den, p-2, p) % p;
return res;
}
4.2 矩阵快速幂
对于递推式如f(n) = af(n-1) + bf(n-2),可以构造转移矩阵:
cpp复制struct Matrix {
ll m[2][2];
Matrix() { memset(m, 0, sizeof(m)); }
};
Matrix multiply(Matrix &a, Matrix &b, ll mod) {
Matrix res;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j)
for (int k = 0; k < 2; ++k)
res.m[i][j] = (res.m[i][j] + a.m[i][k] * b.m[k][j]) % mod;
return res;
}
Matrix fastPow(Matrix a, ll b, ll mod) {
Matrix res;
// 单位矩阵
res.m[0][0] = res.m[1][1] = 1;
while (b > 0) {
if (b & 1) res = multiply(res, a, mod);
a = multiply(a, a, mod);
b >>= 1;
}
return res;
}
4.3 质数检测(Miller-Rabin)
快速幂在Miller-Rabin素性测试中用于实现费马小定理验证:
cpp复制bool isPrime(ll n) {
if (n < 2) return false;
for (ll p : {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}) {
if (n == p) return true;
if (n % p == 0) return false;
ll d = n - 1;
while (d % 2 == 0) d /= 2;
ll x = fastPow(p, d, n);
while (d != n - 1 && x != 1 && x != n - 1) {
x = x * x % n;
d *= 2;
}
if (x != n - 1 && d % 2 == 0) return false;
}
return true;
}
5. 常见错误与调试技巧
5.1 数据范围陷阱
错误示例:
cpp复制int fastPow(int a, int b, int mod) { ... } // 错误!可能溢出
调试方法:
- 使用
typedef long long ll统一处理大数 - 在乘法前添加断言:
assert(a <= INT64_MAX / a)
5.2 边界条件处理
常见错误场景:
- 指数为0时,应返回1%mod(特别注意mod=1时结果为0)
- 底数为0时,0^0在数学中无定义,但竞赛题通常约定为1
5.3 性能优化验证
测试案例:
cpp复制// 测试1e5次1e18量级的运算
auto start = chrono::high_resolution_clock::now();
for (int i = 0; i < 1e5; ++i) {
fastPow(2, 1e18, 1e9+7);
}
auto end = chrono::high_resolution_clock::now();
cout << "Time: " << chrono::duration_cast<chrono::milliseconds>(end-start).count() << "ms";
预期结果:在普通PC上应小于100ms
5.4 模数特殊值处理
当mod为1时,所有结果都为0。有些题目会故意设置这种边界情况,需要特殊处理:
cpp复制ll fastPow(ll a, ll b, ll mod) {
if (mod == 1) return 0;
// ...正常实现
}
6. 竞赛实战案例解析
6.1 NOIP2017提高组原题
题目:计算2^(n^2) mod 1e9+7,其中n≤1e9
解法分析:
- 直接计算n^2会导致n^2达到1e18,普通快速幂无法处理
- 根据费马小定理,当p是质数时,a^(p-1) ≡ 1 (mod p)
- 因此可以先计算n^2 mod (1e9+6),再计算2^result
cpp复制const int MOD = 1e9+7;
const int PHI = MOD-1; // 欧拉函数值
ll n;
cin >> n;
ll exponent = (n % PHI) * (n % PHI) % PHI;
cout << fastPow(2, exponent, MOD);
6.2 CSP-S2020模拟赛题
题目:求(1^1 + 2^2 + ... + n^n) mod m,n≤1e12
优化思路:
- 观察到k^k mod m有周期性,可以找到循环节
- 使用快速幂预处理每个k^k,然后利用周期性求和
cpp复制ll sum = 0;
for (ll k = 1; k <= n; ++k) {
sum = (sum + fastPow(k, k, m)) % m;
// 当检测到循环时可以数学优化
}
cout << sum;
7. 扩展学习与性能对比
7.1 快速幂与内置函数的对比
测试代码:
cpp复制#include <cmath>
// 测试1e7次3^1000000007 mod 1e9+7
benchmark("std::pow", [] {
double res = pow(3, 1000000007);
return (ll)res % 1000000007;
});
benchmark("fastPow", [] {
return fastPow(3, 1000000007, 1000000007);
});
实测结果:
- std::pow:无法处理大指数,结果错误
- fastPow:正确结果647918352,耗时约200ms(1e7次)
7.2 快速乘法的应用
当模数接近LLONG_MAX(如1e18级别)时,乘法也会溢出,此时需要快速乘法:
cpp复制ll fastMul(ll a, ll b, ll mod) {
ll res = 0;
while (b > 0) {
if (b & 1) res = (res + a) % mod;
a = (a + a) % mod;
b >>= 1;
}
return res;
}
ll fastPow(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b & 1) res = fastMul(res, a, mod);
a = fastMul(a, a, mod);
b >>= 1;
}
return res;
}
7.3 常数优化技巧
循环展开优化(以4次为一组):
cpp复制ll fastPow(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
if (b & 1) res = res * a % mod; // 第二组
a = a * a % mod;
b >>= 1;
// 可以继续展开...
}
return res;
}
实测可提升约15%性能,但会降低代码可读性。在NOIP中通常不需要如此极致的优化。