快速幂(一)|小巧而强大的算法

416 阅读5分钟

本文正在参加 人工智能创作者扶持计划

本文阅读时间约8分钟,你将获得:

全网最通俗易懂的快速幂基本思想讲解,不懂不要小钱钱的那种~

快速幂算法的应用将在后续文章进行叙述。

  • 快速幂取模
  • 矩阵快速幂
  • 高精快速幂

算法课的老师的作业,让班里每个人都上去做算法分享的presentation

本来只想讲个分治法浑水摸鱼,没想到同学一个比一个卷,随机森林,梯度下降,密码学......反正一个比一个🐂🍺plus啦,真真就,台下全讲冒泡,台上动规起步呗......听说大佬们还觉得梯度下降太简单,又准备了个plan b ,卷死我算了....

但是作业还是要交的啊,泪奔,我水平有限,就只能剑走偏锋。本文就是记录一下被迫营业的快速幂算法TAT。

简介

快速幂,顾名思义,快速计算幂的算法,也就是说当a或b都很大时,计算aba^b的值或估算其位数的方法,它可能包含了某些动态规划或者分治的思想。

传统计算aba^b可能要做b次乘法,也就是说其时间复杂度是O(n)O(n),而快速幂的算法时间复杂度却能做到O(log(n))O(\log(n)),问题规模越大,快速幂的优势越明显

但:较复杂类型的快速幂的时间复杂度不再是O(logn)O(\log n),它与底数的乘法的时间复杂度有关,此句的解释可见本文递归算法思想一节。

算法思想(非递归)

我看到很多的教程都是从递归开始讲起,但我觉得非递归会更好理解一点。

非递归思想描述

将指数nn分解成二进制数的形式,例如n=(akak1a2a1)2n = (a_k a_{k-1} \cdots a_2 a_1)_2,然后按照二进制位逐个处理,如果当前二进制位为11,则将底数累乘;否则将底数平方。

那么对于任意一个底数a和正整数n,我们都可以使用如下公式计算

an=i=1kaai2i1a^n=\prod\limits_{i=1}^k a^{a_i \cdot 2^{i-1}}

通俗理解

从例子入手,如果问你5135^{13}的值,你会不会将5乘上13次?

但如果将指数nn分解成二进制数的形式:

513=5(1101)2=58×54×515^{13} = 5^{(1101)_2} = 5^{8} \times 5^{4} \times 5^{1}

又因为:

52=(51)2,54=(52)2,58=(54)25^2 = (5^1)^2 , 5^4 = (5^2)^2 , 5^8 = (5^4)^2 \cdots

运算次数=log213=4\therefore 运算次数 = \log_2 13 = 4次

也就是说我们可以将5135^{13}分解为四次运算(13转化为二进制有4位),每次运算的结果都是上一步结果的平方,这样我们就可以利用上一步的结果,快速获得最终值,算法的时间复杂度也因此降到了log(n)\log(n)的级别。

代码实现

// 非递归快速幂
int qpow(int a , int n){
  int res = 1;
  while (n) {
    if (n & 1) res = res * a; // 如果当前位为1,则将a乘到res上
    a = a * a; // a自乘
    n >>= 1; // n右移一位
  }
  return res; // 返回结果
}

// 这个实现使用while循环来迭代地计算a^b 的结果。
// 它从res = 1开始,如果b的当前位为1,则将a乘到res上。
// 它在每次迭代中将a平方并将b除以2(右移一位)。
// 在处理完b的所有位之后,返回最终结果。
// 这个实现的时间复杂度为O(log n)。

算法思想(递归)

递归思想描述

假设要计算xnx^n的值,其中nn为正整数。首先将nn转换为二进制表示,例如n=(1011)2n=(1011)_2,则有:n=23+21+20n=2^3+2^1+2^0 ,接下来,可以通过反复平方的方式,计算出x20,x21,x22,x23x^{2^0},x^{2^1},x^{2^2},x^{2^3}的值,并根据nn的二进制表示,选择将哪些幂次相乘,从而得到xnx^n的值。具体地,算法的流程如下:

  • nn进行二进制分解,得到nn的二进制表示;

  • 初始化x0=1x_0=1x1=xx_1=x

  • nn的二进制表示的最高位开始遍历,对于第ii位:

    • 如果第ii位为0,则将xix_i的值更新为xi=xi12x_i=x_{i-1}^2

    • 如果第ii位为1,则将xix_i的值更新为xi=xi12×xx_i=x_{i-1}^2 \times x

  • 最终的结果为xn=xk×xk1××x0x_n=x_k \times x_{k-1} \times \cdots \times x_0

由于每次迭代都可以将幂次减半,因此算法的时间复杂度为O(logn)O(\log n)。与朴素算法相比,快速幂算法在计算幂次时可以大大减少乘法的次数,从而实现更高效的计算。

上面的叙述可能不是那么容易懂,但它其实是非递归思想的一个逆过程。

通俗理解

更具体地,我们可以用下面的公式来描述快速幂算法:

an={an1a, if n is odd an2an2, if n is even but not 01, if n=0a^n= \begin{cases}a^{n-1} \cdot a, & \text { if } n \text { is odd } \\ a^{\frac{n}{2}} \cdot a^{\frac{n}{2}}, & \text { if } n \text { is even but not } 0 \\ 1, & \text { if } n=0\end{cases}

当然对于上面的公式也可以这样想:

当你计算5135^{13}时可能先使用分治算法,

13是奇数,那我可以计算13mod2+1=7(13 \mod 2 + 1)= 7次,此时的底则变为52=255^2=25,

原问题转化成513=512×5=(52)6×5=256×55^{13} = 5^{12} \times 5 = (5^2)^6 \times 5 = 25^6 \times 5

而这个7次我还可以继续往下分,计算7mod2+1=4(7\mod 2 + 1 )= 4 次 ,此时原问题转化成256×5=(252)3×5=6253×525^6 \times 5 = (25^2)^3 \times 5 = 625^3 \times 5

当然这个4次我还可以继续往下分,原问题转化成6253×5=(625)2×625×5=390625×625×5625^3 \times 5 = (625)^2 \times 625 \times 5 = 390625 \times 625 \times 5

390625×625×5=1220703125 390625 \times 625 \times 5= 1220703125

......

经过上面的演示我们可以看出,使用递归思想的快速幂算法实质上是使用分治法减小幂,同时伴随着底的增大的过程。

这里就引申出另一个问题,虽然问题规模在不断变小,时间复杂度降为了log级别,但当底越来越大时,对其进行平方可能还会受制于大数乘法的时间限制。关于大数乘法相关描述将在后续文章中提及。

代码实现

// 递归快速幂
int qpow(int a , int n){
  if (n == 0) // 如果n为0,则返回1
    return 1;
  else if (n % 2 == 1) // 如果n为奇数,则返回qpow(a, n - 1) * a
    return qpow(a, n - 1) * a;
  else { // 如果n为偶数,则返回qpow(a, n / 2) * qpow(a, n / 2)
    int temp = qpow(a, n / 2);
    return temp * temp;
  }
}

快速幂的应用

快速幂算法可以应用于很多计算中,例如计算幂、取模运算等。

  • 在计算机科学中,快速幂算法可以用于计算大数的幂,例如RSA加密算法。
    • 高精快速幂(洛谷p1045)
  • 在数论中,快速幂算法可以用于计算模幂,
    • 计算abmodca^b \mod c的值。(洛谷p1226)
  • 在动态规划中,快速幂算法可以用于求解斐波那契数列问题。
    • 矩阵快速幂求解fibonacci
  • 在图论中,快速幂算法可以用于计算邻接矩阵的幂。

参考资料

一文彻底搞懂快速幂(原理实现、矩阵快速幂) - bigsai - 博客园 (cnblogs.com)

【竞赛向】斐波那契数列的矩阵快速幂求法_哔哩哔哩_bilibili