浅谈快速幂
这篇随笔简单讲解一下数学问题种快速幂的实现原理及实现。
快速幂的用途
顾名思义,快速幂就是很快速的幂运算,试想当你面对一个问题:求(a^b)的时候,你的第一反应是开(long long)然后用(for)循环一点一点求。那么你就已经会了幂运算的(O(b))算法。按常理来讲,这样的算法已经够用了,但是遇到一些卡时间的题目的时候还是会(T),于是快速幂应运而生。简单地说,快速幂就是一种复杂度为(O(logb))的求幂运算的算法。
快速幂的实现原理
因为快速幂的时间复杂度是(O(logb))的,所以我们自然而然地想到了二进制及位运算。这是显然的,我们知道,一个整数可以被拆分成若干个(2^k)的和。那么类比一下,对于一个幂运算问题(a^b),我们可以把(b)二进制分解,成为若干个(2^k)的和,那么对应下来就是这些和的幂的乘积。
举个例子:
求解问题:(3^{42})。
第一步,将42二进制拆分:
那么,(3^{42})就变成了:
所以,我们就有了这样的一个原理:
在求解(a^b)的时候,如果(b)是奇数,那么原式就是:(a imes a^{b-1})。
同理,如果(b)是偶数,那么原式就可以变成:(a^{bdiv2} imes a^{bdiv 2}).
这是一种倍增的思想,这样我们就把原来的(O(b))算法就被优化成了(O(logb))的算法。
这就是快速幂的实现原理。
快速幂的代码实现
根据我们刚刚学习的快速幂的实现原理,我们很容易发现,这个东西可以用递归来实现。代码如下:
int qpow(int a,int b)
{
if(!b)
return 1;
else if(b&1)
return a*qpow(a,b-1);
else
{
int t=qpow(a,b>>1);
return t*t;
}
}
但是,学过递归的小伙伴应该知道,递归的常数巨大无比。所以上面的代码并不是一个资深(OIer)会选择的东西。
那快速幂怎么写保证常数不大呢?
我们采用一种迭代的写法:
我们会发现,无论(b)为何值,它在快速幂迭代的过程中要么(-1),要么(div 2)。但无论它采用了以上的哪一种操作,都必会有一个时刻,(b=1)。,也就是说,(b)在迭代的过程中,至少会有一个时刻(b)为奇数。
那么我们考虑,我们完全可以在(bdiv 2)的迭代中,先不使迭代的结果影响到答案,而是先把迭代的结果储存下来,然后等到(b)为奇数的时候统一加到答案里去。这样就省去了繁琐的递归和记录答案的过程,保证了常数小,而且维护了答案的正确性。
代码如下:
int qpow(int a,int b)
{
int ret=1;
while(b>0)
{
if(b&1)
ret*=a;
a*=a;
b>>=1;
}
}
return ret;
}
快速乘的原理及其代码实现
其实,就是把快速幂的乘法运算变成了加法运算。
原理超级容易理解...
模板也大同小异:
ll qmult(ll a,ll b)
{
ll ret=0;
while(b>0)
{
if(b&1)
ret=(ret+a)%mod;
a=(a+a)%mod;
b>>=1;
}
return ret;
}