在计算形如 ab(a 的 b 次方)的表达式时,最直接的方法是进行 b-1 次乘法(a * a * ... * a)。这种方法的时间复杂度是 O(b),当 b 非常大时,计算会非常耗时。
快速幂(Fast Exponentiation / Exponentiation by Squaring) 是一种高效计算幂的算法,它巧妙地利用了指数 b 的二进制表示和乘法的结合律,将时间复杂度显著降低到 O(log b)。
核心思想:任何一个正整数 b 都可以表示为其二进制形式。例如,13 的二进制是 1101。那么 a13 = a(8 + 4 + 0 + 1) = a8 * a4 * a1。我们发现,只需要计算 a1, a2, a4, a8, ... 这些底数的平方项,然后根据 b 的二进制位是否为 1,来决定是否将对应的底数平方项乘入最终结果。
指数 的二进制表示 ():
逐步计算过程:
步骤 | 当前检查的二进制位 (从右往左) | 该位是否为 1? | 当前底数 (a2i) | 累乘结果 (res) | 下一步底数 (a * a) |
---|
最终结果:
下面是快速幂算法最常见的迭代(非递归)实现方式。它利用位运算 `(b & 1)` 来判断二进制最低位是否为 1,以及 `(b >>= 1)` 来将指数右移一位(相当于除以 2)。
// C++ 快速幂函数 (非递归)
#include
// 使用 long long 防止中间结果溢出
typedef long long ll;
// 计算 a^b 的结果
ll qpow(ll a, ll b) {
ll res = 1; // 初始化结果为 1 (任何数的 0 次方都是 1)
// 当指数 b 大于 0 时循环
while (b > 0) {
// 检查 b 的当前最低二进制位是否为 1
if (b & 1) { // 等价于 b % 2 == 1
// 如果是 1,则将当前的底数 a 乘入结果 res
res = res * a;
}
// 将底数 a 平方,为下一次迭代做准备 (计算 a^2, a^4, a^8, ...)
a = a * a;
// 将指数 b 右移一位 (相当于 b = b / 2),处理下一位二进制位
b >>= 1; // 等价于 b /= 2
}
// 循环结束后,res 就是 a^b 的结果
return res;
}
int main() {
ll base = 2;
ll exponent = 10;
ll result = qpow(base, exponent);
// 输出:2^10 = 1024
std::cout << base << "^" << exponent << " = " << result << std::endl;
return 0;
}
注意: 这个基础版本没有处理模运算,如果 a 或 b 很大,`res` 和 `a` 可能在计算过程中超出 `long long` 的范围导致溢出。对于大数运算,通常需要结合模运算(见下一页)。
循环的次数取决于指数 b 的二进制位数。一个数 b 的二进制位数大约是 log2b。每次循环内部的操作(位运算、乘法)都是常数时间 O(1)。因此,总时间复杂度为 O(log b)。这比 O(b) 的朴素算法快得多。
该算法只使用了固定数量的额外变量(`res`, `a`, `b` 的副本或自身),与输入规模 b 无关。因此,空间复杂度是 O(1)。
可以看到,只有当 b 的二进制位为 1 时 (对应 21 和 23 位,即指数中的 2 和 8),当时的 a 值 (分别是 4=22 和 256=28) 才被乘入结果。最终结果 = 22 * 28 = 210。
在算法竞赛和实际应用中,直接计算 ab 的结果往往非常巨大,会远远超过标准整型(甚至是 `long long`)的表示范围,导致溢出。然而,很多问题只关心结果对一个特定的数 `m` 取模(取余数)后的值,即计算 (ab) mod m。
幸运的是,模运算具有良好的性质,可以与乘法结合:
(x * y) mod m = ((x mod m) * (y mod m)) mod m
这意味着我们可以在快速幂的每一步乘法之后都进行取模操作,从而保证中间结果和最终结果都不会溢出(只要 m 在整型范围内)。
// C++ 模意义下的快速幂函数
#include
typedef long long ll;
// 计算 (a^b) % mod 的结果
ll qpow_mod(ll a, ll b, ll mod) {
ll res = 1; // 初始化结果为 1
a %= mod; // 预处理:先将底数 a 对 mod 取模,防止 a 本身就很大
// 如果 a 为 0,且 b 不为 0,则结果为 0
// (注意:0^0 通常定义为 1,这里简单处理,实际应用可能需区分)
if (a == 0 && b != 0) return 0;
while (b > 0) {
// 如果当前二进制位为 1
if (b & 1) {
// 将当前底数乘入结果,并立即取模
res = (res * a) % mod;
}
// 底数平方,并立即取模
// 注意:这里 a*a 可能溢出,如果 mod*mod 接近 LLONG_MAX
// 对于极大的 mod,可能需要使用 __int128 或 快速乘 (下面会提到)
a = (a * a) % mod;
// 指数右移
b >>= 1;
}
// 返回最终结果
return res;
}
int main() {
ll base = 3;
ll exponent = 200;
ll modulus = 13;
ll result = qpow_mod(base, exponent, modulus);
// 输出:(3^200) % 13 = 9 (可以通过费马小定理验证 3^12 = 1 mod 13, 200 = 16*12 + 8, 所以 3^200 = 3^8 mod 13)
std::cout << "(" << base << "^" << exponent << ") % " << modulus << " = " << result << std::endl; // 输出 9
return 0;
}
虽然快速幂本身不直接依赖这些定理,但这些定理在处理模幂问题时(特别是降幂)非常有用,常与快速幂结合使用。
指数 的二进制:
逐步计算过程 (结果和底数均对 取模):
步骤 | 当前检查位 | 位是否为 1? | 当前底数 a (mod m) | 累乘结果 res (mod m) | 下一步底数 (a*a) % m |
---|
最终结果:
检验你对快速幂基本概念的理解。
1. 快速幂算法计算 ab 的时间复杂度是多少?
解析:快速幂算法通过每次将指数减半(通过右移位操作)来减少计算次数。循环的次数与指数 b 的二进制位数成正比,即 log2b 次。因此,时间复杂度为 O(log b)。
2. 快速幂算法的核心思想是利用指数 b 的什么特性?
解析:快速幂将指数 b 转换为二进制形式(例如 b = Σ ci * 2i,其中 ci 是 0 或 1)。然后利用 ab = aΣ ci * 2i = Π aci * 2i = Π (a2i)ci。通过计算 a, a2, a4, a8... 并根据 ci 是否为 1 来决定是否乘入结果,从而高效计算。所以核心是利用二进制表示。
3. 在计算 (ab) mod m 时,为了防止中间结果溢出,应该何时进行取模操作?
解析:根据模运算的性质 `(x * y) mod m = ((x mod m) * (y mod m)) mod m`,为了确保所有中间结果都在可控范围内,需要在每次乘法(包括 `res = res * a` 和 `a = a * a`)之后都进行 `% mod` 操作。
4. 在 `while (b > 0)` 循环中,语句 `a = (a * a) % mod;` 的作用是什么?
解析:该语句通过将当前的 a 值平方,得到下一个 2 的幂次方对应的底数。例如,如果当前 a 代表 a2k,执行一次 `a = a * a` 后,新的 a 就代表 (a2k)2 = a2k+1。这使得算法能够高效地得到所有 a2i 的值。
5. 下列哪个场景**不**适合直接使用标准的整数快速幂(可能需要变种或不适用)?
解析: A 可以用模意义下的快速幂。B 可以用矩阵快速幂。C 在计算组合数时,如果涉及除法取模,需要计算逆元,而计算逆元常用费马小定理配合快速幂。D 快速幂算法主要针对整数幂运算,其利用二进制分解指数和整数乘法的性质。对于浮点数,虽然理论上可以类似操作,但精度问题和效率优势可能不如直接使用库函数 `pow()` 或对数/指数函数 `exp(b * log(a))`。
编写一个 C++ 函数 `long long qpow_mod(long long a, long long b, long long mod)`,计算 (ab) mod m 的值。要求处理 a, b, mod 在 `long long` 范围内的情况(假设 `mod * mod` 不会溢出 `long long`)。
**输入样例:** a = 2, b = 1000000000, mod = 1000000007
**输出:** 计算结果
给定四个整数 a, b, m, c,判断等式 (ab) mod m == c 是否成立。其中 a, b, m, c 均在 `long long` 范围内,m > 0。
**输入样例:** a = 3, b = 5, m = 7, c = 5
**输出:** true (因为 35 = 243, 243 mod 7 = 5)
// 快速乘 (用于防止 a * b 溢出 long long)
ll fast_mul(ll a, ll b, ll mod) {
ll res = 0;
a %= mod;
while (b > 0) {
if (b & 1) {
res = (res + a) % mod;
}
a = (a + a) % mod; // 等价于 a = (a * 2) % mod
b >>= 1;
}
return res;
}
// 在 qpow_mod 中使用快速乘
ll qpow_mod_safe(ll a, ll b, ll mod) {
ll res = 1;
a %= mod;
while (b > 0) {
if (b & 1) {
res = fast_mul(res, a, mod); // 使用快速乘
}
a = fast_mul(a, a, mod); // 使用快速乘
b >>= 1;
}
return res;
}
快速幂的思想不仅适用于数值计算,同样可以推广到**矩阵乘法**。如果我们需要计算一个方阵 M 的 N 次幂 (MN),直接 N-1 次矩阵乘法复杂度很高 (通常是 O(N * d3),其中 d 是矩阵维度)。
利用快速幂,我们可以将矩阵 M 看作底数 "a",将指数 N 看作 "b"。矩阵乘法满足结合律,因此可以将 MN 的计算也通过二进制分解 N 来加速。基本步骤与数值快速幂类似:
矩阵乘法的复杂度是 O(d3),快速幂需要 O(log N) 次矩阵乘法,所以总时间复杂度为 O(d3 log N)。
许多线性递推关系,如斐波那契数列 (Fn = Fn-1 + Fn-2),可以通过构造一个**转移矩阵**来表示。例如,对于斐波那契数列:
[ Fn ] = [ 1 1 ] [ Fn-1 ]
[ Fn-1] = [ 1 0 ] [ Fn-2 ]
令状态向量 Vn = [Fn, Fn-1]T,转移矩阵 M = [[1, 1], [1, 0]]。则 Vn = M * Vn-1 = M2 * Vn-2 = ... = Mn-1 * V1。
计算 Fn 就转换为了计算转移矩阵 M 的 n-1 次幂。使用矩阵快速幂,可以在 O(d3 log n) = O(8 log n) = O(log n) 的时间内计算出 Fn(假设 F1, F0 已知)。这对于 N 非常大的情况非常高效。
在模运算中,除以一个数 a 相当于乘以 a 的模逆元 a-1,即满足 (a * a-1) ≡ 1 (mod m)。
模逆元在计算组合数 C(n, k) mod p(p为质数)时非常重要,因为 C(n, k) = n! / (k! * (n-k)!),在模运算下需要计算 k! 和 (n-k)! 的逆元。
快速幂是解决模方程 ax ≡ b (mod p) (其中 p 是质数)的 Baby-Step Giant-Step (BSGS) 算法中的一个子过程。BSGS 算法利用了快速幂和哈希表(或 `std::map`)来在 O(√p) 时间内找到解 x。
类似矩阵快速幂,快速幂的思想也可以用于计算多项式的幂 P(x)n,尤其是在模意义下。这通常需要结合快速傅里叶变换 (FFT) 或数论变换 (NTT) 来加速多项式乘法。
快速幂是一种基础但极其强大的算法工具,通过简单的位运算和平方思想,将幂运算的复杂度从线性降低到对数级别。它不仅能直接解决幂运算问题,更是矩阵运算、线性递推、数论(如求逆元)等众多领域的重要组成部分。
继续探索,不断进步!