快速幂算法

306 阅读4分钟

快速幂Exponentiation by squaring,平方求幂)是一种简单而有效的小算法,它可以以 O(logn)O(logn) 的时间复杂度计算乘方。快速幂不仅本身非常常见,而且后续很多算法也都会用到快速幂。

 

让我们先来思考一个问题:7的10次方,怎样算比较快?

方法1:最朴素的想法,77=49,497=343,... 一步一步算,共进行了9次乘法。

这样算无疑太慢了,尤其对计算机的CPU而言,每次运算只乘上一个个位数,无疑太屈才了。这时我们想到,也许可以拆分问题。

方法2:先算7的5次方,即77777,再算它的平方,共进行了5次乘法。

但这并不是最优解,因为对于“7的5次方”,我们仍然可以拆分问题。

方法3:先算77得49,则7的5次方为4949*7,再算它的平方,共进行了4次乘法。

模仿这样的过程。如果有 xnx^n,且 n=2kn = 2^k,那么原题可以很轻松的表示为:xn=(x2)2)2...x^n=(x^2)^2)^2...。这样只要做k次平方运算就能解决,时间复杂度就从O(n)下降到log(n)。这就是快速幂。

 

 

递归快速幂

刚刚我们用到的,无非是一个二分的思路。我们很自然地可以得到一个递归方程:

an={an1an%2==1an2an2n%2==0,n!=01n=0a^n=\begin{cases}a^{n-1}·a,& {n\%2==1}\\a^\frac{n}{2}·a^\frac{n}{2},& {n\%2==0 , n!=0}\\1 ,& {n=0}\\\end{cases}

计算a的n次方,如果n是偶数(不为0),那么就先计算a的n/2次方,然后平方;如果n是奇数,那么就先计算a的n-1次方,再乘上a;递归出口是a的0次方为1

递归快速幂的思路非常自然,代码也很简单(直接把递归方程翻译成代码即可):

//递归快速幂
int qpow(int a, int n)
{
    if (n == 0)
        return 1;
    else if (n % 2 == 1)
        return qpow(a, n - 1) * a;
    else
    {
        int temp = qpow(a, n / 2);
        return temp * temp;
    }
}

注意,这个temp变量是必要的,因为如果不把 an2a^\frac{n}{2} 记录下来,直接写成qpow(a, n /2)*qpow(a, n /2),那会计算两次 an2a^\frac{n}{2},整个算法就退化为了 O(n)O(n) ,就和正常的连续相乘没什么区别了。这是快速幂的核心所在。

 

递归快速幂求模

在实际问题中,题目常常会要求对一个大素数取模,这是因为计算结果可能会非常巨大,但是在这里考察高精度又没有必要。这时我们的快速幂也应当进行取模,此时应当注意,原则是步步取模,如果MOD较大,还应当开long long

//递归快速幂(对大素数取模)
#define MOD 1000000007
typedef long long ll;
ll qpow(ll a, ll n)
{
    if (n == 0)
        return 1;
    else if (n % 2 == 1)
        return qpow(a, n - 1) * a % MOD;
    else
    {
        ll temp = qpow(a, n / 2) % MOD;
        return temp * temp % MOD;
    }
}

大家知道,递归虽然简洁,但会产生额外的空间开销。我们可以把递归改写为循环,来避免对栈空间的大量占用,也就是非递归快速幂

 

 

非递归快速幂

我们换一个角度来引入非递归的快速幂。还是7的10次方,但这次,我们把10写成二进制的形式,也就是 (1010)2(1010)_2

现在我们要计算 7(1010)27^{(1010)_2} ,可以怎么做?我们很自然地想到可以把它拆分为 7(1000)27(10)27^{(1000)_2}·7^{(10)_2} . 实际上,对于任意的整数,我们都可以把它拆成若干个 7(100...)27^{(100...)_2} 的形式相乘。而这些 7(100...)27^{(100...)_2} ,恰好就是 717^1727^2747^4 ……我们只需不断把底数平方就可以算出它们。

我们先看代码,再来仔细推敲这个过程:

//非递归快速幂
int qpow(int a, int n){
    int ans = 1;
    while(n){
        if(n&1)        //如果n的当前末位为1
            ans *= a;  //ans乘上当前的a。
        a *= a;        //a自乘。  因为2进制中是以1.2.4.8.16为阶梯上涨的,所以直接乘以当前的
        n >>= 1;       //n往右移一位
    }
    return ans;
}

这里的位运算符,>>是右移,表示把二进制数往右移一位,相当于/2;&是按位与,&1可以理解为取出二进制数的最后一位,相当于%2==1。这么一等价,是不是看出了递归和非递归的快速幂的关系了?虽然非递归快速幂因为牵扯到二进制理解起来稍微复杂一点,但基本思路其实和递归快速幂没有太大的出入。

 

非递归快速幂求模
//非递归快速幂
int qpow(int a, int n,int m){
    int ans = 1;
    while(n){
        if(n&1)        //如果n的当前末位为1
            ans = ans*a%m;  //ans乘上当前的a
        a = a*a%m;        //a自乘
        n >>= 1;       //n往右移一位
    }
    return ans;
}

 

 

算法学习笔记(4):快速幂 - Pecco的文章 - 知乎 zhuanlan.zhihu.com/p/95902286