先来看一个问题:
给定三个正整数a、b、m (a<10, b<10,1<m<10),求ab%m。
这只要学过循环就能写出来了,就像下面的代码,时间复杂度是O(b):
typedef long long LL;
LLpow(LL a , LL b , LL m)
{
LL ans = 1;
for(int i=0;i<b;i++)
{
ans = ans * a % m;
}
return ans;
}
代码中使用long long而不用int的原因是防止两个int型变量相乘后溢出。
接下来研究一个更进一步的问题:
给定三个正整数a、b、m (a<103, b<1018, 1<m<109),求ab%m。
对这个问题,如果还是按上面的做法显然是不行的,O(b)的复杂度连支持b< 108都已经很艰难了,更何况1018。
这里要使用快速幂的做法,它基于二分的思想,因此也常称为二分幕。快速幂基于以下事实:
①如果b是奇数,那么有ab=a * a(b-1).
②如果b是偶数,那么有ab=ab/2 * ab/2.
显然,b是奇数的情况总可以在下一步转换为 b是偶数的情况,而b是偶数的情况总可以在下一步转换为b/2的情况。这样,在log(b)级别次数的转换后,就可以把b变为0,而任何正整数的0次方都是1。
举个例子,如果需要求2^10:
①对210来说,由于幂次10为偶数,因此需要先求25,然后有210=25 * 25。
②对25来说,由于幂次5为奇数,因此需要先求24,然后有25 =2 * 24。
③对24来说,由于幂次4为偶数,因此需要先求22,然后有24=22 * 22。
④对22来说,由于幂次2为偶数,因此需要先求21,然后有22=21 * 21。
⑤对21来说,由于幂次1为奇数,因此需要先求20, 然后有21=2 * 20。
⑥20=1,然后从下往上依次回退计算即可。
这显然是递归的思想,于是可以得到快速幂的递归写法,时间复杂度为O(logb):
typedef long long LL;
//求a^b % m , 递归写法
LL binaryPow(LL a , LL b , LL m)
{
if(b==0) return 1; //如果b为0,那么a^0=1
//b为奇数,转换为b-1
if(b % 2 ==0) return a * binaryPow(a , b-1 , m) % m;
else //b为偶数,转换为b/2
{
LL mul = binaryPow(a , b/2 , m);
return mul * mul % m;
}
}
上面的代码中,条件if(b %2 == 1)可以用if(b& 1)代替。这是因为b& 1进行位与操作,判断b的末位是否为1,因此当b为奇数时b&1返回1, if条件成立。这样写执行速度会快一点。
还要注意,当b%2==0时不要返回直接返回binaryPow(a, b/2, m) binaryPow(a, b/2,m),而应当算出单个binaryPow(a, b/2, m)之后再乘起来。这是因为前者每次都会调用两个binaryPow函数,导致复杂度变成O(2^log(b)) = O(b)。例如求binaryPow(8)时, 会变成binaryPow(4) binaryPow(4),而这两个binaryPow(4)都会各自变成binaryPow(2) binaryPow(2),于是就需要求四次binaryPow(2);而每个biaryPow(2)又会变成binaryPow(1) binaryPow(1),因此最后需要求八次binaryPow(1)。
另外,针对不同的题目,可能有两个细节需要注意:
①如果初始时a有可能大于等于m,那么需要在进入函数前就让a对m取模。
②如果m为1,可以直接在函数外部特判为0,不需要进入函数来计算(因为任何正整数对1取模一定等于0)。
接下来研究一下快速幂的迭代写法。
对ab来说,如果把b写成二进制,那么b就可以写成若干二次幂之和。例如13的二进制是1101,于是3号位、2号位、0号位就都是1,那么就可以得到13=23+22 +20=8+4+1,所以a13=a8+4+1=a8 a4 a0。
通过上面的推导,我们发现a13可以表示成a8、a4、a0 的乘积。很容易想象,通过同样的推导,我们可以把任意的ab表示成a2k、… 、a8、a4、a2、a1中若干项的乘积,其中如果b的二进制的i号位为1,那么项a2i就被选中。于是可以得到计算ab的大致思路:令i从0到k枚举b的二进制的每一 位,如果当前位为1,那么累积a2i。注意到序列a2k、… 、a8、a4、a2、a1的前一项总是等于后一项的平方,因此具体实现的时候可以这么做:
①初始令ans等于1,用来存放累积的结果。
②判断b的二进制末尾是否为1 (即判断b& 1是否为1,也可以理解为判断b是否为奇数),如果是的话,令ans乘上a的值。
③令a平方,并将b右移一位(也可以理解为将b除以2)。
④只要b大于0,就返回②。
下面把上面的伪代码写成下面的样子,也就是快速幂的迭代写法:
//c/c++
typedef long long LL;
//求a^b % m , 迭代写法
LL binaryPow(LL a , LL b , LL m)
{
LL ans = 1;
while(b > 0)
{
if(b & 1)
{ //如果b的二进制末尾为1(也可以写成if(b % 2))
ans = ans * a % m; //令ans累积上a
}
a = a * a % m; //令a平方
b >>= 1; //将b的二进制右移1位,即b = b >> 1 或 b = b / 2
}
return ans;
}
#python
def qpow(a,b,mod):
ret = 1
while b:
if(b&1):
ret = ret*a % mod
a = a*a % mod
b>>=1
return ret