news 2026/10/22 6:11:30

快速幂与Strassen矩阵快速幂:从二进制拆分到线性递推

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
快速幂与Strassen矩阵快速幂:从二进制拆分到线性递推

很多初学者第一次写求幂函数,十有八九是长这样的:

def naive_pow(a, n): res = 1 for _ in range(n): res *= a return res

代码本身没什么毛病,但它经不起大指数的考验:n 一旦上亿,这个循环就跑不动了。我第一次被这个问题卡住是在某次课程设计里,要求计算 2^k 对 10^9+7 取模,k 可以大到 10^18。我当时写了上面这个循环,等了大半天没结果,后来才意识到问题不是程序错了,而是算法复杂度根本没有救。真正该做的,是把指数拆成二进制,用平方代替连乘——这就是快速幂。

这篇文章把整数快速幂和 Strassen 矩阵快速幂一起讲透。前者负责解决"大数 n 次方"的效率问题,后者把同一套分治思想搬到矩阵上,顺手解决斐波那契数列、线性递推、图上路径计数这一大类问题。无论你是准备算法竞赛、应付数据结构考试,还是工作中遇到大指数运算,这篇都值得仔细读一遍。

1. 连乘为什么慢:从二进制视角看指数

1.1 循环连乘的复杂度短板

回到 naive_pow。它的时间复杂度是 O(n),也就是 n 次乘法。当 n 是 10、100 的时候毫无压力,可一旦 n 进入 10^9 甚至 10^18 的量级,这个复杂度就直接判死刑。做个粗略估算:现代 CPU 一秒钟大概能执行 10^9 次级别的简单运算,n=10^9 的循环至少要跑一秒以上,n=10^18 就意味着几十亿年的计算量,这在工程上等于不可计算。

所有涉及大指数的严肃场景——RSA 类的模幂运算、大数计算、组合数学里的快速求幂——全都依赖快速幂,不可能靠循环硬撑过去。理解这一点之后,再看快速幂的每个设计决策,思路都会清晰很多。

1.2 二进制拆分:13 = 8 + 4 + 1 带来的顿悟

核心洞察其实一句话就能说清:任何一个正整数 n 都可以写成 2 的幂之和。拿 13 举例,13 的二进制是 1101,也就是 13 = 8 + 4 + 1。于是:

a^13 = a^8 · a^4 · a^1

接下来注意到一个递推关系:a^1、a^2、a^4、a^8 每一项都是前一项的平方。也就是说,我不用真的去乘 13 个 a,只需要从 a^1 开始平方三次就能得到所有候选值:

  • a^2 = (a^1)²
  • a^4 = (a^2)²
  • a^8 = (a^4)²

平方三次得到四个候选值,再把二进制位为 1 的那些挑出来相乘:a^8 · a^4 · a^1,乘三次。总共 6 次乘法,比 13 次少了一半多。指数越大,这个差距越夸张。

拿 3^13 演算一遍:13 = 1101₂,我们从 3 开始平方,3² = 9,9² = 81,81² = 6561。二进制位为 1 的是 1、4、8 这三个,所以结果 = 3 × 81 × 6561 = 1594323,和 3^13 完全一致。这个例子很朴素,但已经完整展示了快速幂的全部机制。

1.3 分治公式与复杂度推导

把上面的思路整理成递归公式:

f(a, 0) = 1 f(a, n) = f(a, n/2)² n 为偶数 f(a, n) = f(a, n/2)² · a n 为奇数

每一层递归都把 n 砍一半,所以层数是 O(log n),每层最多做两次乘法,总复杂度 O(log n)。把 n=10^18 代进去,log2(10^18) 约等于 60,也就是说大概 60 层递归、一两百次乘法就能出结果。从 10^18 次乘法降到约 10^2 次乘法,差了整整 16 个数量级。

这个公式看起来简单,但它其实是整篇文章的地基。注意推导过程从头到尾只用了乘法的结合律和"乘以 1 不变"这两条性质,完全没有依赖任何"数"特有的规则。这意味着,只要一个运算系统满足结合律并且存在单位元,这套分治办法就成立——矩阵就是这个系统里最重要的一个。

2. 整数快速幂的两种写法与三个边界坑

2.1 递归版和迭代版,我推荐后者

递归版代码最短,逻辑也最贴合分治公式:

def fast_pow_recursive(a, n): if n == 0: return 1 half = fast_pow_recursive(a, n // 2) if n % 2 == 0: return half * half return half * half * a

但我实际写代码,几乎总是用迭代版。原因有两个。第一,递归调用本身有栈帧开销,虽然 log2(10^18) 只有 60 层,远不会爆掉 Python 默认的递归深度,但工程上能省一点是一点。第二,迭代版的循环结构直接对应"扫描指数的二进制位",可读性反而更好,调试起来也直观:

def fast_pow(a, n): result = 1 while n > 0: if n & 1: result *= a a *= a n >>= 1 return result

循环体里就三件事:看最低位是否为 1、决定要不要把当前的 a 乘进 result、然后让 a 自乘、n 右移一位。循环结束时 result 恰好就是所有二进制位为 1 的那些 a^(2^i) 的乘积。这个写法建议大家直接背下来,它几乎是所有快速幂变体的公共骨架。

2.2 取模版本:两个防溢出细节

真实场景里几乎没人直接求完整幂,因为 a^n 指数增长,几轮平方就超过 64 位整数上限。最常见的需求是求 a^n mod m,模运算对乘法是友好的——先乘再取模和边乘边取模结果一样,所以每步都能压模:

long long mod_pow(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; } a = (__int128)a * a % mod; n >>= 1; } return result; }

这里有两个特别容易翻车的细节。第一,result 初始化写成 1 % mod 而不是 1,专门处理 mod = 1 的极端情况:任何数对 1 取模都是 0,但 1 % 1 才是 0,直接初始化成 1 会返回错误结果。第二,乘法用 __int128 中转,因为两个接近 10^9 的数相乘会到 10^18 量级,long long 上限约 9.2×10^18,虽然单次乘法通常不爆,但多个累加就可能出问题,用 128 位中转最稳妥。

如果你的编译器不支持 __int128,可以用"快速乘"把乘法拆成加法和位运算来避免溢出。原理很简单:乘法本质上就是多次加法,而取模运算对加法同样保序,所以把乘数按二进制拆开累加,每次加完立刻取模,就能让中间值始终保持在安全范围。这是以时间换空间的写法,但关键时候能救命。

2.3 三个边界:n=0、负数指数、底数为 0

我调试快速幂踩过的坑,几乎都集中在边界条件上。

  • n=0:任何非零底数的 0 次方是 1,循环体不执行,result 必须初始化为 1。如果当初把 result 初始化成 0,n=0 时直接返回错误答案。
  • 负数指数:整数快速幂的 while 循环面对负数是死循环,因为右移永远不为 0。处理方式取决于场景:实数域直接 1.0 / fast_pow(a, -n);模质数域则借助费马小定理,先算 a^(p-2) 得到 a 的逆元,再对逆元做快速幂,这样就能计算负指数的模意义结果。
  • 底数 a 是 mod 的倍数:此时 a % mod = 0,只要 n > 0 结果必然是 0;但 n = 0 时结果是 1。所以函数入口处最好先判断 n == 0,再做 a %= mod,避免底数取模变成 0 后把幂的结果全部带偏。

另外有一条实战经验:底数很大时,一定要在进循环前先 a %= mod。我曾在某个项目里忘了这行,底数是一个几千位的随机数,前几轮 result 直接溢出,结果怎么都不对,排查了很久才发现 bug 出现在函数第一行,而不是算法逻辑。这种低级错误只要形成习惯就不会再犯。

整数快速幂的坑处理完,下一步就是把同一个模板搬到矩阵上。你会发现代码几乎不需要改动,变的只是"数"变成了"矩阵"。

3. Strassen 矩阵快速幂:从数到矩阵的分治推广

3.1 为什么矩阵可以原样套用快速幂

回顾第一节末尾那句话:快速幂的所有推导只依赖乘法结合律和单位元存在。矩阵乘法满足结合律,(AB)C = A(BC),而且 n 阶单位矩阵 E 满足 EA = AE = A。所以对矩阵 A 求 A^n,公式和整数完全同构:

A^0 = E A^n = (A^(n/2))² n 为偶数 A^n = (A^(n/2))² · A n 为奇数

唯一的新增注意点是矩阵乘法不满足交换律,所以"乘 A"的方向要统一。快速幂实现里我们始终用 res = res * A 这个方向,不要写成 A * res。原理在于:展开过程中底数始终是同一个矩阵 A,结合律保证括号怎么加都对;但一旦交换了顺序,结果就可能完全不一样。这个坑对刚接触矩阵快速幂的人非常容易踩,一定要在代码里固化写乘法时的参数顺序。

3.2 "Strassen"这个称呼:先说清两种算法

标题里的"Strassen"值得单独解释一下,因为中文资料对这两个概念经常混着用。

一方面,1969 年提出的 Strassen 矩阵乘法算法,使用分治策略把 2×2 分块矩阵的乘法从 8 次实数乘法降到 7 次,从而把矩阵乘法复杂度从 O(n³) 优化到约 O(n^2.807)。另一方面,很多教程和题解里的"Strassen 法求矩阵幂",指的是"对指数做二进制分解、对矩阵做平方"的这套快速幂框架,它本身不涉及 7 次乘法那套优化,只是名字挂在这里。

这两种算法一个针对指数分治,一个针对矩阵乘法本身分治,容易混淆。我的建议是先把基础版矩阵快速幂吃透——内层使用标准 O(k³) 矩阵乘法,整体复杂度 O(k³ log n),这覆盖了 95% 以上的实际需求。等真遇到 k 特别大(比如上百)且矩阵不太稀疏时,再把内层乘法替换成 Strassen 乘法,把复杂度降到 O(k^2.807 log n)。注意 Strassen 乘法常数非常大,k 小于某个阈值(一般几百)时反而比标准乘法更慢,盲目上 Strassen 属于典型的负优化。

提示:算法竞赛和大多数工程场景里矩阵规模 k 基本不超过 100,标准矩阵乘法完全够用。不要为了"听起来高级"而选择 Strassen 乘法,先把基础模板写对。

3.3 完整实现与实现细节

下面是一个可直接复制的 C++ 矩阵快速幂通用模板:

#include <vector> using namespace std; using Matrix = vector<vector<long long>>; const long long MOD = 1000000007LL; Matrix mat_mul(const Matrix& A, const Matrix& B) { int n = A.size(); int m = B.size(); int p = B[0].size(); Matrix C(n, vector<long long>(p, 0)); for (int i = 0; i < n; ++i) { for (int k = 0; k < m; ++k) { if (A[i][k] == 0) continue; long long v = A[i][k]; for (int j = 0; j < p; ++j) { C[i][j] = (C[i][j] + v * B[k][j]) % MOD; } } } return C; } Matrix mat_pow(Matrix A, long long exp) { int n = A.size(); Matrix res(n, vector<long long>(n, 0)); for (int i = 0; i < n; ++i) res[i][i] = 1; while (exp > 0) { if (exp & 1) res = mat_mul(res, A); A = mat_mul(A, A); exp >>= 1; } return res; }

细节挨个说清楚。res 初始化为单位矩阵,对角线全 1,对应整数版本里的 result = 1,这是"什么都没乘"时的单位元。res = mat_mul(res, A) 对应"二进制位为 1 时乘入 A",A = mat_mul(A, A) 对应"平方",三个语句的顺序和方向都不能乱。

mat_mul 里我加了 if (A[i][k] == 0) continue,对稀疏矩阵是很好的常数优化。斐波那契这种小矩阵里 0 元素占比高,实测能省两成左右的时间。要注意的是 MOD=10^9+7 时,v * B[k][j] 的结果约 10^18,还在 long long 范围内;如果 mod 更大,这里同样要换成 __int128 中转。

4. 矩阵快速幂的三类典型应用

4.1 斐波那契:最经典的入门案例

二阶线性递推 F(n) = F(n-1) + F(n-2) 可以写成矩阵乘法的形式:

[F(n) ] [1 1] [F(n-1)] [F(n-1)] = [1 0] × [F(n-2)]

反复代入右侧,可以得到:

[F(n) ] [1 1]^(n-1) [F(1)] [F(n-1)] = [1 0] [F(0)]

于是求第 n 项斐波那契数就变成了求矩阵 M = [[1,1],[1,0]] 的 n-1 次幂,复杂度从 O(n) 直接降到 O(log n)。我第一次看到这个转化时觉得特别惊艳:一个看似只能逐个递推的问题,通过"状态转移矩阵"的抽象,变成了一个可以快速幂的问题。

下面给一份完整可跑的 Python 参考:

def mat_mul(A, B, mod): n, m, p = len(A), len(B), len(B[0]) C = [[0] * p for _ in range(n)] for i in range(n): for k in range(m): if A[i][k] == 0: continue for j in range(p): C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % mod return C def mat_pow(A, exp, mod): n = len(A) res = [[0] * n for _ in range(n)] for i in range(n): res[i][i] = 1 while exp > 0: if exp & 1: res = mat_mul(res, A, mod) A = mat_mul(A, A, mod) exp >>= 1 return res def fib(n, mod=10**9 + 7): if n == 0: return 0 M = [[1, 1], [1, 0]] T = mat_pow(M, n - 1, mod) return T[0][0]

注意 fib(0) 必须单独判断:如果 n = 0 时还去算 M^(-1),快速幂遇到负数指数会直接出问题。这个 bug 我在帮同学看代码时见过不止一次,几乎都是 n 的边界没处理好。

4.2 任意 k 阶线性递推:伴侣矩阵的构造

斐波那契只是特例。任何形如 F(n) = c1·F(n-1) + c2·F(n-2) + ... + ck·F(n-k) 的 k 阶线性齐次递推,都能用同一个模板解决。构造方法叫伴侣矩阵,结构长这样:

[F(n) ] [c1 c2 c3 ... ck] [F(n-1) ] [F(n-1) ] [1 0 0 ... 0 ] [F(n-2) ] [F(n-2) ] = [0 1 0 ... 0 ] [F(n-3) ] [... ] [................] [.... ] [F(n-k+1) ] [0 0 ... 1 0 ] [F(n-k) ]

第一行是递推公式本身,下面每一行负责把状态向量往下"移动"一位。构造这个矩阵不需要手抄:第一行按顺序填系数,然后从第二行开始把 (i, i-1) 位置填 1,其余位置填 0,循环一遍就能完成。

举个例子,三阶递推 F(n) = F(n-1) + F(n-2) + F(n-3),矩阵就是:

[F(n) ] [1 1 1] [F(n-1)] [F(n-1)] = [1 0 0] × [F(n-2)] [F(n-2)] [0 1 0] [F(n-3)]

有了这个模板,大量递推题都可以直接套 mat_pow。如果递推式还带常数项,比如 F(n) = c1·F(n-1) + c(非齐次),只要把常数也扩进状态向量,多加一个"永远等于 1"的维度,同样能转成齐次形式。这种"加维度"的思路在状态机模型里特别常见。

4.3 邻接矩阵的幂:图上有多少条 k 步路径

矩阵快速幂另一个反直觉的应用是图论路径计数。设图 G 的邻接矩阵为 A,A[i][j] = 1 表示存在从 i 到 j 的边。那么 A^k 中第 i 行第 j 列的元素,恰好等于从 i 出发走恰好 k 步到达 j 的路径总数。

证明用归纳法很简洁:A² 的 [i][j] 等于 Σ_t A[i][t]·A[t][j],这正好是所有经过中间点 t 的两步路径数之和;A³ 就是再乘一次 A,把两条路径拼接成三条。所以"求 k 步路径条数"本质上就是"求矩阵 k 次幂",直接套 mat_pow 即可。

这个性质在工程里的应用比想象中广泛:社交网络中的"k 度关系"挖掘、推荐系统里用户到商品的间接关联强度、知识图谱中两个实体之间的路径依赖度,都可以抽象成邻接矩阵的幂。当然现实场景图很大,矩阵往往是稀疏的,需要配合稀疏矩阵存储和幂迭代算法,这里不展开,但核心思想是完全一致的。

5. 实战选型:什么时候用快速幂,什么时候别硬上

5.1 三种做法的时间复杂度对照

求 a^n 的方式乘法次数n=10^12 时的大致量级适用场景
朴素循环连乘n10^12n 较小,几乎用不上
整数快速幂约 2·log2(n)约 80绝大多数大指数求幂
矩阵快速幂(k 阶矩阵)约 2·log2(n)·k³约 80·k³线性递推、路径计数

n = 10^12 时,朴素循环要一万亿次乘法,现代 CPU 也得跑到地老天荒;整数快速幂大概 80 次乘法,瞬间出结果。矩阵快速幂多出来的 O(k³) 来自矩阵乘法本身,这就是为什么遇到递推题要先问一句:k 是多大?

如果 k 是 1000,k³ 就是 10^9,叠加 log n 的 40,整体是 4×10^10 次乘法量级。这种规模硬上矩阵快速幂就不太明智,要么利用矩阵稀疏性做优化,要么换多项式取模等其他方法。

5.2 快速幂救不了的场景

快速幂不是银弹。我总结过三类"硬上反而更亏"的情况。

第一类是指数 n 很小。n 小于几十时,直接循环连乘可能比矩阵快速幂更快,因为矩阵构造和函数调用本身有固定开销,省下的几次乘法不足以回本。第二类是递推式带非线性或卷积项,比如 F(n) = F(n-1) + Σ F(i)·F(n-i),这种没法写成"常数矩阵乘以状态向量"的形式,需要生成函数、CDQ 分治或 FFT 优化这些更专门的工具。第三类是矩阵规模过大,k 上万时 O(k³ log n) 完全不现实,必须利用循环矩阵、稀疏矩阵、带状矩阵的特殊结构做乘法优化,否则连内存都放不下。

另外补充一个细节:有些递推可以用快速幂但没必要。比如 n 只有 10^5 而且要输出所有中间项时,线性递推一遍扫过去是 O(nk),矩阵快速幂反而因为要反复构造矩阵而显得笨重。选择算法的顺序永远是先看数据范围,再看能否归约到已知模板,最后才是考虑常数优化。

5.3 两个实测有效的常数优化

最后分享两个我实测有效的优化技巧。

第一个,快速幂主循环里不要把平方写成 if/else 两个分支。很多新手会写成"如果 n 是奇数,先乘再平方;否则只平方",其实平方无论如何都要做,只需要在 n 为奇数时把当前的 A 乘进 res,然后无条件执行 A = mat_mul(A, A)。写成下面这样最干净:

while (exp > 0) { if (exp & 1) res = mat_mul(res, A); A = mat_mul(A, A); exp >>= 1; }

第二个,对于固定的小矩阵,比如 2×2 斐波那契矩阵,手写结构体展开乘法比通用三层循环快一个数量级。通用模板胜在可读性和灵活性,但如果程序里要跑成千上万次矩阵快速幂,固定小矩阵值得手写展开:

struct Mat2 { long long a00, a01, a10, a11; }; Mat2 mul2(const Mat2& x, const Mat2& y, long long mod) { return { (x.a00 * y.a00 + x.a01 * y.a10) % mod, (x.a00 * y.a01 + x.a01 * y.a11) % mod, (x.a10 * y.a00 + x.a11 * y.a10) % mod, (x.a10 * y.a01 + x.a11 * y.a11) % mod, }; }

这套东西我用了很多年,最大的体会是:快速幂真正值钱的不是那十几行代码,而是"把指数拆成二进制、把重复连乘换成分治平方"这个思维模型。整数用它,矩阵用它,状态机转移、线性递推、路径计数最后都归一成同一个模板。把这个模板吃透,你会发现自己看许多难题的视角都不一样了——很多看似复杂的模型,本质上不过是一次求幂而已。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 6:11:30

JavaScript包管理器供应链攻击揭秘:从漏洞成因到项目加固实践

包管理器&#xff0c;是 JavaScript 生态的“水电煤”&#xff0c;也是供应链攻击的突破口。npm、yarn、pnpm 这些热门包管理器几乎承担了所有前端项目的依赖分发工作&#xff0c;但正因为它们被无条件信任&#xff0c;一旦管理器本身出现漏洞&#xff0c;恶意代码就能顺着依赖…

作者头像 李华
网站建设 2026/10/22 6:10:22

claude-mem 记忆机制解析:从抽取到召回,构建对话持久化记忆层

1. 从零理解 claude-mem 到底在解决什么问题第一次看到 claude-mem 这个名字&#xff0c;很多人会下意识以为它又是一个"给对话加个记忆开关"的小工具。但真正用过一段时间之后你会发现&#xff0c;它想解决的是一个更底层、也更让人头疼的问题&#xff1a;如何让一次…

作者头像 李华
网站建设 2026/10/22 6:10:03

AI乳腺超声筛查系统:从技术原理到临床落地的完整拆解

最近&#xff0c;某国家级肿瘤研究机构发布的乳腺癌筛查综述论文里&#xff0c;引用了我全程参与开发的AI乳腺超声筛查系统。看到那一段引文的时候&#xff0c;说实话我比当年拿到项目验收结论还激动。很多圈外朋友问&#xff1a;AI看乳腺超声&#xff0c;是不是实验室里自嗨&a…

作者头像 李华
网站建设 2026/10/22 6:09:19

MySQL 5.7 GTID与多线程复制:构建高可用主从架构实战

某电商项目组的订单库主库在晚高峰前突然宕机&#xff0c;运维和DBA登录服务器时系统已经完全不响应。按照当时的应急预案&#xff0c;大家需要找出主库最后写入的binlog文件名和position位置&#xff0c;再手动把从库提升上来。整个过程花了四十多分钟&#xff0c;业务侧一直报…

作者头像 李华
网站建设 2026/10/22 6:09:07

ESP32-S3实战:环境搭建、RGB屏幕驱动与AI向量指令

1. 为什么我会在2024年放弃ESP32、转身买了一块S3开发板我玩ESP32也有三年多了&#xff0c;从最早的ESP32经典版一直用到ESP32-S2&#xff0c;平时做点家庭自动化的小板子、传感器采集器&#xff0c;基本够用。但今年在做一个带屏幕的桌面天气站项目时&#xff0c;突然觉得两个…

作者头像 李华
网站建设 2026/10/22 6:07:06

STM32高级定时器互补PWM输出:实现180°反相与死区可调

如果你调过逆变器、电机驱动器或者任何带功率半桥的电路&#xff0c;大概率遇到过这类需求&#xff1a;同一组上下桥臂需要两路PWM驱动&#xff0c;波形反相、相位差严格180&#xff0c;占空比还得跟着控制指令随时变。直接拿两路独立的定时器输出&#xff0c;逻辑上能凑出反相…

作者头像 李华