快速幂

autor: iamtwz, billchenchina, CBW2007, CCXXXI, chinggg, Enter-tainer, eyedeng, FFjet, gaojude, Great-designer, H-J-Granger, Henry-ZHR, hsfzLZH1, Ir1d, kenlig, Konano, ksyx, luoguyuntianming, Marcythm, Menci, NachtgeistW, ouuan, Peanut-Tang, qwqAutomaton, sshwy, StudyingFather, Tiphereth-A, TrisolarisHD, TRSWNCA, Xeonacid, Yuuko10032, Zhangjiacheng2006, Zhoier, Hszzzx, shenshuaijie, kfy666

引入

快速幂(fast exponentiation),也称 二进制取幂(binary exponentiation)或 平方取幂法(exponentiation by squaring),是一个在 Θ(log⁡n)\Theta(\log n) 的时间内计算 ana^n 的小技巧,而暴力的计算需要 Θ(n)\Theta(n) 的时间.

这个技巧可以应用于任何 aa 的乘法满足结合律的场景中,例如模意义下取幂、矩阵幂等,详见后文 应用 一节.

过程

计算 aa 的 nn 次方表示将 nn 个 aa 乘在一起:an=a×a⋯×a⏟n 个 aa^{n} = \underbrace{a \times a \cdots \times a}_{n\text{ 个 a}}.然而当 nn 太大或单次乘法开销太大的时候,这种方法就不太适用了.二进制取幂的想法是,将取幂的任务按照指数的 二进制表示 来分割成更小的任务.

例子

假设要计算 3133^{13}.如果将它展开为连乘式,需要 13−1=1213-1=12 次乘法.但是,因为

313=3(1101)2=38×34×31,3^{13} = 3^{(1101)_2} = 3^8 \times 3^4 \times 3^1,

所以,只要能快速计算出 31,32,34,383^{1},3^{2},3^{4},3^{8},就能通过 22 次乘法计算出 3133^{13} 的值.于是,只需要知道一个快速的方法来计算上述 33 的 2k2^k 次幂的序列.这是容易的,因为序列中(除第一个)任意一个元素都是其前一个元素的平方.

根据这些分析,可以得到 3133^{13} 的计算过程如下:

31=3,32=(31)2=32=9,34=(32)2=92=81,38=(34)2=812=6561,313=6561×81×3=1594323.\begin{aligned} 3^1 &= 3, \\ 3^2 &= \left(3^1\right)^2 = 3^2 = 9, \\ 3^4 &= \left(3^2\right)^2 = 9^2 = 81, \\ 3^8 &= \left(3^4\right)^2 = 81^2 = 6561, \\ 3^{13} &= 6561 \times 81 \times 3 = 1594323. \end{aligned}

过程中,只进行了 55 次乘法运算.

这就是快速幂的基本想法.至于具体实现,有两种常见的版本.

迭代版本

设 nn 的二进制表示为 (ntnt−1⋯n1n0)2(n_tn_{t-1}\cdots n_1n_0)_2,也就是说,有

n=nt2t+nt−12t−1+⋯+n121+n020,n = n_t2^t + n_{t-1}2^{t-1} + \cdots + n_12^1 + n_02^0,

其中,ni∈{0,1}n_i\in\{0,1\}.那么,就有

an=ant2t+nt−12t−1+⋯+n121+n020=an020×an121×⋯×ant−12t−1×ant2t.\begin{aligned} a^n & = a^{n_t2^t + n_{t-1}2^{t-1} + \cdots + n_12^1 + n_02^0}\\ & = a^{n_0 2^0} \times a^{n_1 2^1}\times \cdots \times a^{n_{t-1}2^{t-1}} \times a^{n_t2^t}. \end{aligned}

注意,只有 ni=1n_i=1 的项才会真正出现在乘积的计算中.

根据这一表达式,可以首先在 Θ(log⁡n)\Theta(\log n) 时间内计算出 aa 的 Θ(log⁡n)\Theta(\log n) 个 2k2^k 次幂的取值,然后花费 Θ(log⁡n)\Theta(\log n) 的时间选择等于 11 的二进制位对应的幂次乘到最终结果中.这就是快速幂的迭代版本实现.

伪代码如下:

Algorithm FastPow(a,n):Input. Base a and exponent n.Output. Power an.Method.1result←Id2while n>0 do3if n mod 2=1 then4result←result⋅a5end if6a←a⋅a7n←n/28end while9return result\begin{array}{l} \textbf{Algorithm }\text{FastPow}(a, n): \\ \textbf{Input. }\text{Base }a\text{ and exponent }n.\\ \textbf{Output. }\text{Power }a^n.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \textit{result}\gets\mathrm{Id}\\ 2 & \textbf{while }n > 0\textbf{ do}\\ 3 & \qquad \textbf{if }n \bmod 2 = 1\textbf{ then}\\ 4 & \qquad \qquad \textit{result} \gets \textit{result}\cdot a\\ 5 & \qquad \textbf{end if}\\ 6 & \qquad a \gets a \cdot a\\ 7 & \qquad n \gets n / 2\\ 8 & \textbf{end while}\\ 9 & \textbf{return }\textit{result} \end{array} \end{array}

利用这一方法计算快速幂,需要进行 Θ(log⁡n)\Theta(\log n) 次乘法运算.

递归版本

这一过程同样可以通过递归形式实现.注意到,指数 nn 的二进制展开可以递归地写作

(ntnt−1⋯n1n0)2=2×(ntnt−1⋯n1)2+n0.(n_tn_{t-1}\cdots n_1n_0)_2 = 2 \times (n_tn_{t-1}\cdots n_1)_2 + n_0.

因此,幂次 ana^n 可以递归地计算为

an={1,n=0,(a⌊n/2⌋)2,n>0 and n is even,(a⌊n/2⌋)2⋅a,n>0 and n is odd.a^n = \begin{cases} 1, & n = 0,\\ (a^{\lfloor n/2\rfloor})^2, & n > 0 \text{ and }n\text{ is even},\\ (a^{\lfloor n/2\rfloor})^2\cdot a, & n > 0 \text{ and }n\text{ is odd}.\\ \end{cases}

这就是快速幂的递归版本实现.

伪代码如下:

Algorithm FastPow(a,n):Input. Base a and exponent n.Output. Power an.Method.1if n=0 then2return Id3end if4result←FastPow(a,n/2)5if n mod 2=0 then6return result⋅result7else8return result⋅result⋅a9end if\begin{array}{l} \textbf{Algorithm }\text{FastPow}(a, n): \\ \textbf{Input. }\text{Base }a\text{ and exponent }n.\\ \textbf{Output. }\text{Power }a^n.\\ \textbf{Method.}\\ \begin{array}{ll} 1 & \textbf{if }n = 0\textbf{ then}\\ 2 & \qquad \textbf{return }\mathrm{Id}\\ 3 & \textbf{end if}\\ 4 & \textit{result} \gets \text{FastPow}(a, n / 2) \\ 5 & \textbf{if }n\bmod 2 = 0\textbf{ then}\\ 6 & \qquad \textbf{return }\textit{result}\cdot\textit{result}\\ 7 & \textbf{else}\\ 8 & \qquad \textbf{return }\textit{result}\cdot\textit{result}\cdot a\\ 9 & \textbf{end if} \end{array} \end{array}

利用这一方法计算快速幂,需要递归 Θ(log⁡n)\Theta(\log n) 次,同样需要 Θ(log⁡n)\Theta(\log n) 次乘法运算.尽管复杂度相同,由于递归本身有一定开销,所以实践中迭代版本的速度更快.

应用

模意义下取幂

洛谷 P1226【模板】快速幂

给定三个整数 a,b,pa,b,p,求 ab mod pa^b\bmod p.其中 p≥2p\ge 2.

这是一个非常常见的应用,例如它可以用于计算模意义下的乘法逆元.既然我们知道取模的运算不会干涉乘法运算,因此我们只需要在计算的过程中取模即可.

首先我们可以直接按照上述递归方法实现:

参考实现

C++

long long binpow(long long a, long long b, long long p) {
  if (b == 0) return 1;
  long long res = binpow(a, b / 2, p);
  if (b % 2)
    return res * res % p * a % p;
  else
    return res * res % p;
}

Python

def binpow(a, b, p):
    if b == 0:
        return 1
    res = binpow(a, b // 2, p)
    if (b % 2) == 1:
        return res * res * a % p
    else:
        return res * res % p

第二种实现方法是非递归式的.它在循环的过程中将二进制位为 1 时对应的幂累乘到答案中.尽管两者的理论复杂度是相同的,但第二种在实践过程中的速度是比第一种更快的,因为递归会花费一定的开销.

参考实现

C++

long long binpow(long long a, long long b, long long p) {
  long long res = 1;
  while (b > 0) {
    if (b & 1) res = res * a % p;
    a = a * a % p;
    b >>= 1;
  }
  return res;
}

Python

def binpow(a, b, p):
    res = 1
    while b > 0:
        if b & 1:
            res = res * a % p
        a = a * a % p
        b >>= 1
    return res
注意
  • 模数通常情况下大于 11.在十分特殊的情况下,模数 pp 可能等于 11,此时需要特殊考虑 b=0b=0 的情况.
  • 当指数很大时,需利用 扩展欧拉定理 降幂后计算.

计算斐波那契数

根据斐波那契数列的递推式 Fn=Fn−1+Fn−2F_n = F_{n-1} + F_{n-2},我们可以构建一个 2×22\times 2 的矩阵来表示从 Fi,Fi+1F_i,F_{i+1} 到 Fi+1,Fi+2F_{i+1},F_{i+2} 的变换.于是在计算这个矩阵的 nn 次幂的时候,我们使用快速幂的思想,可以在 Θ(log⁡n)\Theta(\log n) 的时间内计算出结果.对于更多的细节参见 斐波那契数列,矩阵快速幂的实现参见 矩阵加速递推 中的实现.

多次置换

问题描述

给你一个长度为 nn 的序列和一个置换,把这个序列置换 kk 次.

简单地把这个置换取 kk 次幂,然后把它应用到序列上即可.时间复杂度为 O(nlog⁡k)O(n \log k).对于更多的细节参见 置换的复合.

注意

对这个置换建图,然后在每一个环上分别做 kk 次幂(事实上等价于 kk 对环长取模),可以在 O(n)O(n) 的时间复杂度下解决此问题.

加速几何中对点集的操作

HDU 4087 A Letter to Programmers

给定三维空间中 nn 个点 pip_i,要求将 mm 个操作都应用于这些点.包含 3 种操作:

  1. 沿某个向量移动点的位置(Shift).
  2. 按比例缩放这个点的坐标(Scale).
  3. 绕某条直线旋转(Rotate).

还有一个特殊的操作,就是将某个操作序列重复 kk 次(Repeat),Repeat 操作可以嵌套.输出操作结束后每个点的坐标.

参考 向量与矩阵 中的内容,每一种操作都可以用一个变换矩阵表示,一系列连续的变换可以用矩阵的乘积来表示.一个 Repeat 操作就相当于取一个矩阵的 kk 次幂.这样可以用 O(mlog⁡k)O(m \log k) 的时间计算出整个变换序列最终形成的矩阵.最后将它应用到 nn 个点上,总复杂度 O(n+mlog⁡k)O(n + m \log k).

定长路径计数

问题描述

给一个有向图(边权为 1),求任意两点 u,vu,v 间从 uu 到 vv,长度为 kk 的路径的条数.

我们把该图的邻接矩阵 MM 取 kk 次幂,那么 Mi,jM_{i,j} 就表示从 ii 到 jj 长度为 kk 的路径的数目.该算法的复杂度是 O(n3log⁡k)O(n^3 \log k).有关该算法的细节请参见 矩阵 页面.

模意义下的整数乘法

问题描述

给定非负整数 a,ba,b 和正整数 mm,计算 a×b mod ma\times b\bmod m,其中 a,b≤m≤1018a,b\le m\le 10^{18}.

与二进制取幂的思想一样,这次我们将其中的一个乘数表示为若干个 2 的整数次幂的和的形式.因为在对一个数做乘 2 并取模的运算的时候,我们可以转化为加减操作防止整型溢出.这样可以在 O(log⁡m)O (\log m) 的时间复杂度下解决问题.递归方法如下:

a⋅b={0if a=02⋅a2⋅bif a>0 and a even2⋅a−12⋅b+bif a>0 and a odda \cdot b = \begin{cases} 0 &\text{if }a = 0 \\ 2 \cdot \frac{a}{2} \cdot b &\text{if }a > 0 \text{ and }a \text{ even} \\ 2 \cdot \frac{a-1}{2} \cdot b + b &\text{if }a > 0 \text{ and }a \text{ odd} \end{cases}

但在实际使用中,此方法由于引入了更大的计算复杂度导致时间效率不优.实际编程中通常利用 快速乘 来进行模数范围在 long long 时的乘法操作.

高精度快速幂

前置技能:大整数乘法

洛谷 P1045 [NOIP 2003 普及组] 麦森数

给定整数 PP(1000<P<31000001000 < P < 3100000),计算 2P−12^P−1 的位数与最后 500500 位数字(用十进制数表示),不足 500500 位时高位补 0.

代码实现
#include <cmath>
#include <cstring>
#include <iostream>
using namespace std;

const int M = 500;

int a[505], b[505], t[505];

// 大整数乘法
void mult(int x[], int y[]) {
  memset(t, 0, sizeof(t));
  for (int i = 1; i <= x[0]; i++) {
    for (int j = 1; j <= y[0]; j++) {
      if (i + j - 1 > M) continue;
      t[i + j - 1] += x[i] * y[j];
      t[i + j] += t[i + j - 1] / 10;
      t[i + j - 1] %= 10;
      t[0] = i + j;
    }
  }
  memcpy(b, t, sizeof(b));
}

// 快速幂
void binpow(int p) {
  if (p == 1) {
    memcpy(b, a, sizeof(b));
    return;
  }
  binpow(p / 2);  // (2^(p/2))^2=2^p
  mult(b, b);     // 对 b 平方
  if (p % 2 == 1) mult(b, a);
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  int p;
  cin >> p;
  a[0] = 1;  // 记录 a 数组的位数
  a[1] = 2;  // 对 2 进行平方
  b[0] = 1;  // 记录 b 数组的位数
  b[1] = 1;  // 答案数组
  binpow(p);
  cout << (int)(log10(2) * p) + 1 << '\n';
  b[1] -= 1;  // 最后一位减 1
  for (int i = M; i >= 1; i--) {
    cout << b[i];
    if ((i - 1) % 50 == 0) {
      cout << '\n';
    }
  }
}

底数固定的预处理快速幂

当底数 aa 固定时,可以利用 分块思想,用一定的时间预处理后用 O(1)O(1) 的时间回答一次幂询问.这一算法也常称为光速幂.过程如下:

  1. 选定一个数 ss,预处理出 a0,a1,⋯ ,as−1a^0,a^1,\cdots,a^{s-1} 与 a0,as,⋯ ,a⌊p/s⌋sa^0,a^s,\cdots,a^{\lfloor p/s\rfloor s} 的值并存在两个数组里;
  2. 对于每一次询问 aba^b,将 bb 拆分成 ⌊b/s⌋s+(b mod s)\lfloor b/s\rfloor s+(b\bmod s),则 ab=a⌊b/s⌋s⋅ab mod sa^b=a^{\lfloor b/s\rfloor s}\cdot a^{b\bmod s},就可以 O(1)O(1) 求出答案.

假设指数 bb 的范围是 [0,n][0,n],那么块长 ss 经常选择为 n\sqrt{n} 或者与之相近的 22 的幂次.选择 n\sqrt{n} 可以获得最优的预处理复杂度 O(n)O(\sqrt{n}),而选择 22 的幂次可以使用位操作简化计算.

特别地,对于模意义下幂的计算,底数 aa 相同隐含着模数 mm 也要相同这一要求.由于 扩展欧拉定理,对于任意模数 mm,预处理的指数范围上界为 n=2φ(m)n = 2\varphi(m);对于素模数 pp,预处理的范围上界为 n=p−1n = p - 1.这两种情形预处理的复杂度都是 O(m)O(\sqrt{m}).

参考代码
int a, mod, pow1[65536], pow2[65536];

void preproc() {
  pow1[0] = pow2[0] = 1;
  for (int i = 1; i < 65536; i++) pow1[i] = 1LL * pow1[i - 1] * a % mod;
  int pow65536 = 1LL * pow1[65535] * a % mod;
  for (int i = 1; i < 65536; i++) pow2[i] = 1LL * pow2[i - 1] * pow65536 % mod;
}

int query(int pows) {
  return 1LL * pow1[pows & 65535] * pow2[pows >> 16] % mod;
}

习题

本页面部分内容译自博文 Бинарное возведение в степень 与其英文翻译版 Binary Exponentiation.其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0.

本文内容来自 OI Wiki《快速幂》;作者与修订历史见原文。AIOJ 对排版、站内链接和资源路径做了格式适配。

内容依 CC BY-SA 4.0 与 SATA 许可共享;附加条款以原文声明为准。感谢 OI Wiki 作者与贡献者;请按 SATA 约定前往 OI Wiki 仓库 Star 支持。