C++快速幂完整实战讲解
1. 背景与意义
指数运算(a^n)是数学与计算机科学中最常见的操作之一。直观的计算方法是把基数 a 乘 n 次,但是当 n 很大(如数十亿、上亿甚至 10<sup>18</sup>)时,线性循环的时间复杂度 O(n) 将无法接受。为此,快速幂 (又名"左移右乘"或 "指数平方法")提供了一种将时间复杂度降低到 O(log n) 的技术。
在多数算法竞赛、密码学、数论与线性代数等领域,快速幂往往是必备技术。例如:
- 计算模幂 (
pow_mod) 用于朴素加密/解密、RSA 中的大数运算; - 计算阶乘模幂用来实现
C(n, k)的快速求法; - 解决
a^b % m的竞赛题目; - 计算矩阵平方幂实现快速求斐波那契数、线性递推等。
因为其运算简单、代码可读性高、且兼容几乎所有编程语言,快速幂已成为电脑学者的标配技巧。
2. 快速幂的核心思想
快速幂基于以下拆解公式:
an={an/2×an/2如果 n 是偶数an−1×a如果 n 是奇数an={an/2×an/2an−1×a如果 n 是偶数如果 n 是奇数
可进一步写成:
an=an/2×an/2若 n 为偶(a(n−1)/2×a(n−1)/2)×a若 n 为奇an=an/2×an/2(a(n−1)/2×a(n−1)/2)×a若 n 为偶若 n 为奇
这个拆分利用了 二进制 的特性:从高位到低位逐步处理指数二进制位。具体实现有两大变体:
- 递归实现:直接按上面公式递归求解;
- 迭代实现 :利用指数的最低有效位,累积结果,同时把指数右移一位(
n >>= 1),把基数平方。
两种实现都在 O(log n) 的时间复杂度内完成指数运算。
3. 递归实现
3.1 基本代码
cpp
long long powerRec(long long a, long long n) {
if (n == 0) return 1; // 任何数的 0 次方为 1
long long half = powerRec(a, n >> 1);
if (n & 1) // 奇数
return half * half * a;
else // 偶数
return half * half;
}
3.2 关键点解读
n == 0为递归终止条件。n >> 1相当于n / 2,位运算比除法快。n & 1判断n是否为奇数。- 递归深度最多
⌊log₂ n⌋ + 1,对n < 10^18的情况,只需不到 64 层递归。这在大多数现代编译器下完全安全(递归深度通常 1e5 或更高)。
3.3 递归背后的数值问题
- 整数溢出 :
half * half * a可能产生 64 位溢出。若仅需要在不取模的情况下求伪值,使用__int128或long double可缓解;若已知需要取模,可将half与a先模,以保证中间结果不大。 - 无负指数 :上面代码仅适用于
n ≥ 0。若想支持负指数,需要使用浮点数或将题目转成求分数的形式。
4. 迭代实现
迭代实现是快速幂中最常用的形式,代码更简洁,递归带来的函数调用开销也消失。
cpp
long long powerIter(long long a, long long n) {
long long result = 1;
while (n > 0) {
if (n & 1) // 当前最低位为 1
result *= a; // 乘以对应的基数
a *= a; // 基数平方
n >>= 1; // 移除最低位
}
return result;
}
4.1 迭代不需要递归
- 空间复杂度:常量 O(1)。
- 时间复杂度 :同递归,为
O(log n)。 - 可读性:大多数程序员更倾向于迭代版本,易于在代码审查时定位错误。
4.2 迭代与递归的比较
| 特点 | 递归 | 迭代 |
|---|---|---|
| 空间 | O(log n)(递归栈) |
O(1) |
| 性能 | 额外递归函数调用 | 纯循环 |
| 可读性 | 直观,符合公式 | 简洁 |
| 安全性 | 递归深度偶尔超限 | 无关 |
在极端大数(如 n > 10^9)的情况下,递归方式完全无问题;但如果你在嵌入式或最小化内存场景,迭代更稳妥。
5. 取模实现(模幂)
当我们需要求 a^n mod m(特别是 m 为 10<sup>9</sup>+7 等 32 位或 64 位素数)时, 直接做 half * half 计算会溢出。我们可以在每一步都做模运算,保证中间值不会太大。
cpp
long long modpow(long long a, long long n, long long mod) {
long long result = 1 % mod;
a %= mod;
while (n > 0) {
if (n & 1)
result = (__int128)result * a % mod; // __int128 防止 64 位乘溢出
a = (__int128)a * a % mod;
n >>= 1;
}
return result;
}
5.1 关键技巧
- 使用
__int128:当a与mod都接近 10<sup>18</sup> 时,a * a会溢出 64 位。__int128(若编译器支持)可以安全处理 128 位整数。 - 按位取模 :
a %= mod先对基数做模,随后所有乘法保持在[0, mod)范围内,避免乘溢出。 - 取模的顺序:一定先做乘,再做取模,而不是先取模后再乘。
5.2 典型应用场景
| 需求 | 模幂实现 |
|---|---|
1. 计算 a^b mod M(如 M = 1e9+7) |
modpow(a, b, M) |
2. 计算组合 C(n, k) mod M |
用费马小定理,C(n, k) = fact[n] * invfact[k] * invfact[n-k] mod M,其中 invfact 是 modpow(fact, M-2, M) 的结果 |
3. 计算斐波那契数 F(n) mod M |
用矩阵快速幂,矩阵元素同样做模运算 |
6. 栈与缓存:优化可读性与性能
6.1 将模幂抽成函数模板
在多语言项目中,模幂经常被多处调用。使用模板可以让编译器生成类型安全的代码。
cpp
template<typename T, typename U>
T modpow(T a, U n, T mod) {
T res = 1 % mod, base = a % mod;
while (n > 0) {
if (n & 1) res = (__int128)res * base % mod;
base = (__int128)base * base % mod;
n >>= 1;
}
return res;
}
- 这里我们允许
n为更宽的整数类型(unsigned long long),但求值时会自动转换。 - 若使用
uint64_t,__int128需要unsigned __int128。
6.2 递归式的合并
如果需要在递归中间做模,比如:
cpp
T modpowRec(T a, U n, T mod) {
if (n == 0) return 1 % mod;
T half = modpowRec(a, n >> 1, mod);
if (n & 1) return (__int128)half * half % mod * a % mod;
else return (__int128)half * half % mod;
}
注意 half 先做过模,防止溢出。
7. 负指数与浮点数
7.1 负整数指数
在整数域上,负指数意味着求 a 的求逆(如果 a 可逆)。若 mod 为素数,可以借助费马小定理求逆:
a−n≡(a−1)n(modm)a−n≡(a−1)n(modm)
其中 a^{-1} = a^{m-2} \bmod m.
cpp
long long modpowNeg(long long a, long long n, long long mod) {
// 先取 a 的逆
long long inv_a = modpow(a, mod - 2, mod); // precondition: mod is prime
return modpow(inv_a, n, mod);
}
7.2 浮点指数
如果题目要求 x^n(其中 x 为浮点数,不取模),可以使用 std::pow。但其实快速幂也能配合浮点数使用:
cpp
double powFloat(double a, long long n) {
double res = 1.0;
while (n > 0) {
if (n & 1) res *= a;
a *= a;
n >>= 1;
}
return res;
}
与 std::pow 相比,它在说明"快速幂"时更显直观。虽然 sqrt、log 等更高级实现会被优化,但对于一般竞赛来说差别不大。
8. 常见错误与陷阱
- 忘记取模 :若没有 MOD,
a * a在大数时会溢出。 - 使用
int而不是long long:尤其在 OJ 的INT_MAX之后测试数据会导致错误。 - 链式乘法 :
result = result * a;先乘后取模,但若result过大会溢出。 - 负数模 :在 C++ 中
-1 % mod结果为-1,而我们通常需要正模结果。可在取模后加mod再% mod。 - 复合模(多模):若需要满足多个模,使用 CRT(Chinese Remainder Theorem)或将每一步都做模。
- 取
n的二进制时使用unsigned long long:避免因为负数右移产生负号。
9. 复杂度分析
- 时间 :
- 递归 & 迭代:
Θ(log₂ n)。 - 若
n = 10^18,循环次数约为 60。 - 乘法
__int128速度稍慢,但可接受。
- 递归 & 迭代:
- 空间 :
- 迭代:
O(1)。 - 递归:
O(log n)(约 60 层栈)。
- 迭代:
- 对
mod的限制 :如果mod远小于 2<sup>63</sup>,__int128能保证中间结果不溢。
10. 进阶应用:矩阵快速幂
矩阵幂同样可以使用快速幂思想。设 A 为 n × n 矩阵,则:
An=An/2×An/2若 n 为偶(A(n−1)/2×A(n−1)/2)×A若 n 为奇An=An/2×An/2(A(n−1)/2×A(n−1)/2)×A若 n 为偶若 n 为奇
实现通常采用自定义 Matrix 结构,并提供 multiply(按模运算)和 power(迭代或递归)。
cpp
struct Matrix {
static const int N = 3; // 例:3x3
long long a[N][N];
Matrix identity() {
Matrix I;
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
I.a[i][j] = (i == j);
return I;
}
Matrix operator*(const Matrix& other) const {
Matrix res;
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j) {
__int128 sum = 0;
for (int k = 0; k < N; ++k)
sum += (__int128)a[i][k] * other.a[k][j];
res.a[i][j] = (long long)(sum % MOD);
}
return res;
}
Matrix pow(long long exp) const {
Matrix result = identity();
Matrix base = *this;
while (exp > 0) {
if (exp & 1) result = result * base;
base = base * base;
exp >>= 1;
}
return result;
}
};
10.1 用法示例:斐波那契数
斐波那契数可表示为:
(Fk+1Fk)=(1110)k(F1F0)(Fk+1Fk)=(1110)k(F1F0)
所以 F_n = (M^n)[0][0]。
cpp
long long fib(long long n, long long mod) {
Matrix M;
M.a[0][0] = 1; M.a[0][1] = 1;
M.a[1][0] = 1; M.a[1][1] = 0;
return M.pow(n).a[0][0];
}
双倍出-斐波那契(F_{2k} 作为 F_k * (2*F_{k+1} - F_k)) 亦可利用快速幂来实现。
11. 预先计算与组合
11.1 取模逆元素
在组合数求模时,需要用到阶乘的逆元。根据费马小定理:
x−1≡xm−2(modm)(m 为素数)x−1≡xm−2(modm)(m 为素数)
所以我们可以预先计算 fact 与 invfact:
cpp
const int MAXN = 1e6;
long long fact[MAXN], invfact[MAXN];
void precompute() {
fact[0] = 1;
for (int i = 1; i < MAXN; ++i)
fact[i] = fact[i-1] * i % MOD;
invfact[MAXN-1] = modpow(fact[MAXN-1], MOD-2, MOD);
for (int i = MAXN-2; i >= 0; --i)
invfact[i] = invfact[i+1] * (i+1) % MOD;
}
long long C(int n, int k) {
if (k < 0 || k > n) return 0;
return fact[n] * invfact[k] % MOD * invfact[n-k] % MOD;
}
此方法在 O(1) 时间即可求任意 C(n, k) (n < 1e6)。
11.2 相关变形
- 求组合数 :
C(a + b, a),C(n, k) - 指数组合 :
nCr = (n! / r! / (n-r)!) mod M - 圆盘堆叠 :常出现的
C(n + k - 1, k)或C(n, k),同样使用预处理。
12. 负数模、粘模
有时模数 M 不是素数,或者是 int 负数。C++ 的取模运算对负数的行为为"保持符号",导致 -1 % 100 得到 -1,而我们通常想得到 99。处理办法:
cpp
long long modFix(long long x, long long mod) {
return (x % mod + mod) % mod;
}
或者在每次取模前做 ((x % mod) + mod) % mod。
13. 结合多模 & CRT
在有些题目中,需要 a^n mod m1、a^n mod m2 等多组模运算结果合并为一个大数。此时可使用中国剩余定理(CRT):
- 先用快速幂得到各模下的结果
r1, r2, ..., rk。 - 通过 CRT 将六个结果合并为一组:
cpp
long long CRT(vector<long long> residues, vector<long long> mods) {
long long x = 0, M = 1;
for (size_t i = 0; i < residues.size(); ++i) {
long long m = mods[i];
long long ri = residues[i];
long long t = ((__int128)(ri - x) * modpow(M, m-2, m)) % m;
x += M * t;
M *= m;
}
return x;
}
M 必须在 __int128 范围内,否则会溢出。
14. 取值范围与边界考虑
| 场景 | 取值范围 | 推荐实现 |
|---|---|---|
int < 2<sup>31</sup> |
< 2<sup>31</sup> | 迭代,使用 long long 进行乘法 |
long long < 2<sup>63</sup> |
< 2<sup>63</sup> | __int128 乘法 |
| 需要正模 | > 2<sup>63</sup> | __int128 + mod 取模 |
取模素数 1e9+7 |
1e9+7 | long long 足够,long long * long long 生产64 位,__int128 仍是安全最佳 |
| 非素数 mod | 任何 | 同自行限制,中间乘可以到 mod^2,需 __int128 |
选择 __int128 是最保险的做法;若编译器不支持,可拆分成 64 位乘法,或使用第三方大整数库 (如 Boost.Multiprecision::cpp_int)。但大多竞赛的编译器(GCC 9+)都支持 __int128。
15. 代码集成与范例
以下给出一个完整、可直接跑通的 CPP 示例,涵盖快速幂、取模、矩阵幂、组合数求模、负指数处理与 CRT,并在
main函数里演示多种用法。
cpp
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007LL;
/* ---------- Fast Modular Power ---------- */
long long modpow(long long a, long long n, long long mod = MOD) {
long long res = 1 % mod;
a %= mod;
while (n > 0) {
if (n & 1)
res = (__int128)res * a % mod;
a = (__int128)a * a % mod;
n >>= 1;
}
return res;
}
/* ---------- Negatives & Positive Mod & CRT helper ---------- */
long long mod_fix(long long x, long long mod) {
return (x % mod + mod) % mod;
}
long long crt(const vector<long long>& rs, const vector<long long>& ms) {
long long x = 0, M = 1;
for (size_t i = 0; i < rs.size(); ++i) {
long long m = ms[i];
long long r = rs[i];
long long t = (__int128)(r - x) * modpow(M, m-2, m) % m;
x = (x + M * t) % (M * m);
M *= m;
}
return x;
}
/* ---------- Matrix 3x3 exemplar ---------- */
struct Matrix {
static const int SZ = 3;
long long a[SZ][SZ];
Matrix() { memset(a, 0, sizeof(a)); }
static Matrix identity() {
Matrix I;
for (int i = 0; i < SZ; ++i) I.a[i][i] = 1;
return I;
}
Matrix operator*(const Matrix& o) const {
Matrix r; long long tmp;
for (int i = 0; i < SZ; ++i)
for (int j = 0; j < SZ; ++j) {
__int128 sum = 0;
for (int k = 0; k < SZ; ++k)
sum += (__int128)a[i][k] * o.a[k][j];
r.a[i][j] = (long long)(sum % MOD);
}
return r;
}
Matrix pow(long long e) const {
Matrix r = identity(), b = *this;
while (e > 0) {
if (e & 1) r = r * b;
b = b * b;
e >>= 1;
}
return r;
}
};
Matrix make_stirling() { // 3x3 example matrix
Matrix M;
M.a[0][0] = 0; M.a[0][1] = 1; M.a[0][2] = 0;
M.a[1][0] = 0; M.a[1][1] = 0; M.a[1][2] = 1;
M.a[2][0] = 1; M.a[2][1] = 1; M.a[2][2] = 0;
return M;
}
/* ---------- Precomputation for nCr ---------- */
const int LIM = 2000000;
long long fact[LIM+1], invfact[LIM+1];
void precompute_fact() {
fact[0] = 1;
for (int i = 1; i <= LIM; ++i) fact[i] = fact[i-1] * i % MOD;
invfact[LIM] = modpow(fact[LIM], MOD-2);
for (int i = LIM; i > 0; --i) invfact[i-1] = invfact[i] * i % MOD;
}
inline long long C(int n, int k) {
if (k < 0 || k > n) return 0;
return fact[n] * invfact[k] % MOD * invfact[n-k] % MOD;
}
/* ---------- Main demo ---------- */
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
/* ① 快速幂常规用法 */
long long a = 1234567890123LL;
long long n = 1234567890LL;
cout << "a^n mod MOD = " << modpow(a, n) << '\n';
/* ② 负指数的计算(模数必须是素数) */
long long inv_a = modpow(a % MOD, MOD-2); // a^-1
long long pos_n = 100; // a^100
long long neg_n = modpow(inv_a, 100); // a^-100 mod MOD
cout << "a^-100 mod MOD = " << neg_n << '\n';
/* ③ 矩阵幂支持:S(-1)^n(示例) */
Matrix M = make_stirling();
Matrix Mn = M.pow(10);
cout << "M^10[2][2] = " << Mn.a[2][2] << '\n';
/* ④ 预处理阶乘、逆阶乘,快速组合数 */
precompute_fact();
cout << "C(10,3) = " << C(10,3) << '\n'; // 120
cout << "C(2000000,1000) = " << C(2000000,1000) << '\n';
/* ⑤ 正负模以及 CRT 合并示例 */
long long r1 = modpow(2, 100, 3); // 2^100 mod 3
long long r2 = modpow(2, 100, 5); // 2^100 mod 5
vector<long long> rs = {r1, r2};
vector<long long> ms = {3, 5};
cout << "CRT(2^100 mod 15) = " << crt(rs, ms) << '\n';
return 0;
}
上述代码已通过编译器(GCC 12.2 / Clang 14)测试,涵盖了常见场景。若您在 OJ / 代码库中使用,可直接拷贝。
16. 性能微调与实战注意
- 循环展开 :若
n较大,可以把while (n)循环拆成十几条if (n & 1) ...,减少分支开销。 - 关闭 I/O 同步 :在大 I/O 场景下,使用
ios::sync_with_stdio(false); cin.tie(nullptr);。 - 编译优化 :开启
-O3或-march=native,保证编译器把__int128乘法优化为mul+sal/shl。 - 使用模板 :若需要对多种整数(
int32,int64,__int128)做快速幂,可写成模板。 - 避免不必要的
% mod:在顶层循环里,如果a已经小于mod,不必再次取模。 - 实验不同实现 :若时间吃紧,可用递归 加 尾递归保存 (
inline+register)或将递归改写成for ( ; n; n = n >> 1)的方式。 - 剖析特殊情况 :特别是
mod == 1时,无论何种指数,结果都是 0;判断mod == 1可以直接返回 0,避免modpow里取模操作导致 0/0。
17. 结语
快速幂是解决指数问题的底层构件 。一次完整、细致的 C++ 实现不只是一个单纯的"算法实现",更是对 算术运算、位运算、循环、递归、模板、整数溢出、取模、矩阵、组合、负数处理、CRT 等多方面知识的综合体现。
无论是 算法竞赛简单题 (如 3^1000 % 1e9+7)、中等难度题 (矩阵斐波那契)、高阶题 (多模合并 + 线性递推)还是 科研项目(大数据模运算),掌握快速幂的实现原则与细节都是不可或缺的。
只要能写出一个 完整、没有黑洞、可运行、并且自带测试 的实现,就意味着你已具备高质量代码 的基础与能力。祝你在后续的算法旅程中不断发掘更多应用场景,再次感谢阅读!