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),是一个在 的时间内计算 的小技巧,而暴力的计算需要 的时间。

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

过程

计算 的 次方表示将 个 乘在一起:。然而当 太大或单次乘法开销太大的时侯,这种方法就不太适用了。二进制取幂的想法是,将取幂的任务按照指数的 二进制表示 来分割成更小的任务。

例子

假设要计算 。如果将它展开为连乘式,需要 次乘法。但是,因为

所以,只要能快速计算出 ,就能通过 次乘法计算出 的值。于是,只需要知道一个快速的方法来计算上述 的 次幂的序列。这是容易的,因为因为序列中(除第一个)任意一个元素都是其前一个元素的平方。

根据这些分析,可以得到 的计算过程如下:

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

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

迭代版本

设 的二进制表示为 ,也就是说,有

其中,。那么,就有

注意,只有 的项才会真正出现在乘积的计算中。

根据这一表达式,可以首先在 时间内计算出 的 个 次幂的取值,然后花费 的时间选择等于 的二进制位对应的幂次乘到最终结果中。这就是快速幂的迭代版本实现。

伪代码如下:

利用这一方法计算快速幂,需要进行 次乘法运算。

递归版本

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

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

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

伪代码如下:

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

应用

模意义下取幂

给定三个整数 ,求 。其中 。

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

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

参考实现

[list2tab]

  • 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 时对应的幂累乘到答案中。尽管两者的理论复杂度是相同的,但第二种在实践过程中的速度是比第一种更快的,因为递归会花费一定的开销。

参考实现

[list2tab]

  • 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

[!warning] 注意

  • 模数通常情况下大于 。在十分特殊的情况下,模数 可能等于 ,此时需要特殊考虑 的情况。
  • 当指数很大时,需利用 扩展欧拉定理 降幂后计算。

计算斐波那契数

根据斐波那契数列的递推式 ,我们可以构建一个 的矩阵来表示从 到 的变换。于是在计算这个矩阵的 次幂的时侯,我们使用快速幂的思想,可以在 的时间内计算出结果。对于更多的细节参见 斐波那契数列,矩阵快速幂的实现参见 矩阵加速递推 中的实现。

多次置换

问题描述

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

简单地把这个置换取 次幂,然后把它应用到序列上即可。时间复杂度为 。对于更多的细节参见 置换的复合。

注意

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

加速几何中对点集的操作

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

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

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

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

定长路径计数

问题描述

给一个有向图(边权为 1),求任意两点 间从 到 ,长度为 的路径的条数。

我们把该图的邻接矩阵 取 次幂,那么 就表示从 到 长度为 的路径的数目。该算法的复杂度是 。有关该算法的细节请参见 矩阵 页面。

模意义下的整数乘法

问题描述

给定非负整数 和正整数 ,计算 ,其中 。

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

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

高精度快速幂

前置技能:大整数乘法

给定整数 (),计算 的位数与最后 位数字(用十进制数表示),不足 位时高位补 0。

底数固定的预处理快速幂

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

  1. 选定一个数 ,预处理出 与 的值并存在两个数组里;
  2. 对于每一次询问 ,将 拆分成 ,则 ,就可以 求出答案。

假设指数 的范围是 ,那么块长 经常选择为 或者与之相近的 的幂次。选择 可以获得最优的预处理复杂度 ,而选择 的幂次可以使用位运算简化计算。

特别地,对于模意义下幂的计算,底数 相同隐含着模数 也要相同这一要求。由于 扩展欧拉定理,对于任意模数 ,预处理的指数范围上界为 ;对于素模数 ,预处理的范围上界为 。这两种情形预处理的复杂度都是 。

参考代码

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。

此文件夹下有0条笔记。