浅谈快速幂/快速乘
浅谈快速幂
这篇随笔简单讲解一下数学问题种快速幂的实现原理及实现。
快速幂的用途
顾名思义,快速幂就是很快速的幂运算,试想当你面对一个问题:求\(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\times a^{b-1}\)。
同理,如果\(b\)是偶数,那么原式就可以变成:\(a^{b\div2}\times a^{b\div 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\)为奇数。
那么我们考虑,我们完全可以在\(b\div 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;
}