【OI】关于快速幂的简单理解

快速幂:因为a+b=c => n^a * n^b = n^c

所以n^m可以分解为n^m=n^a1 * n^a2 * n^a3... * n^ak ,a1+a2+a3...+ak=m

所以我们想到,如果存在k<m,并且可以以O(k)的复杂度求出n^a1、n^a2、n^a3...n^ak中的每一个数,就可以大大的优化运算的时间复杂度。

我们都知道二进制转十进制时,是对每一位呈上位权,而计算机可以对十进制数进行位运算,通过位运算可求出二进制的每一位,。。。tobe continue

都知道算某个数的幂需要线性的复杂度,为了优化复杂度,就出现了所谓的快速幂。

快速幂的代码很短,但是要原理需要一点心思。

首先,我们知道, 

a^b = a^c * a^d (c+d=b)

那么,不就可以通过 a^b = a^b1 * a^b2 * a^b3... * a^bn (b1 + b2...+bn = b) 来快速获得a^b吗?这个方法的优越性在于,如果可以线性的求出a^b1~a^bn,不就是很快的算法吗?

因为a^b=a^c*a^d,c+d=b这条原理,我们的目标是找到普遍满足 b = b1 + b2 ... + bn的规律。所以,我们必须要找出一个统一的方法来确定b对应的b1~bn分别是多少。

如果各位知道进制转换的原理,就可以知道:一个n进制的数转为十进制等于 求和( i = 0~n-1 ) n ^ i * 第 i 位的数字。

例如,十进制数10的二进制数1010可以表示为: 2^0 * 0 + 2^1 * 1 + 2^2 * 0 + 2^3 * 1 (这里挺绕的,为了方便理解和验证,2^3*1意思是 2进制,第3位,二进制数字1010中第三位是1)

仔细一想,这不就是我们要的"确定b对应的b1~bn分别是多少"吗?这也是快速幂的原理所在,将一个质数分解为许多可以线性一个个求出的2的次方的幂。

我再次重申a^b=a^c*a^d,c+d=b这条原理,不能不搞清楚乘法和加法的关系,我们之前得到的加法规律实际上是应用于c+d=b这里的,最后的计算还是要用乘法。

之前我一直在说,这个方法或者说规律的优越性在于我们可以线性的求出相加的每一个指数。

例如,我知道十进制数10,也就是二进制数1010,我就可以在线性时间复杂度里得到 2^0 * 0 、 2^1 * 1 、 2^2 * 0 、 2^3 * 1。

说了这么多废话,目的在于接下来的这一条原理。

( a^(2^0) )^2 =   a^(2^1) 你可以亲自验证一下这条神奇的性质。不仅是对0+1 = 1有效,你可以把0换成任何一个数,把1换成那个数+1,看看还会不会生效。       

对于每个数k,归纳一下,就是 (a^(2^k) )^2 = a^(2^(k+1) ) 。其实自己稍微一想,就明白是怎么回事了。

在初中数学中,我们知道 (a^b)^k = a^(b*k)。   所以,(a^(2^k) )^2 = a^( (2^k)*2 )。而2^k = 相乘(i = 1~k) 2。(2*2*2*2...*2,k个2),所以(2^k)*2也就是“k个2相乘再乘2”不就是“k+1个2”,2^(k+1)么。
总结一下完整的推导过程, (a^(2^k) )^2 = a^(2^k)*2 = a^(2^(k+1) )。
实际上这个推导非常简单,写出来是为了清晰思路,根据题意更好利用快速幂利用的性质。
 
 
最后剩下一个点没有说,那就是在二进制中,每一位只有1和0两种状态,在0的时候看看我们之前的分析,实际上是没有作用的(理解为指数相加时+0,或者相乘时乘1),为了不让它“滥竽充数”地也来分一杯羹,我们使用&运算符,判断这个二进制最后一位是否为0,如果是0我们就不能在最后乘上它。当然,在看到代码之前看这句话自然是一头雾水,我们接下来看代码:
 
typedef long long ll;
ll poww(ll a, ll b)//a^b { ll re = 1; while(b != 0) { if(b & 1) re *= a; a = a * a; b >>= 1; } return re; }

解释一下代码。

b & 1代表着我们之前的判断"为了不让它“滥竽充数”地也来分一杯羹,我们使用&运算符,判断这个二进制最后一位是否为0"。为什么要判断“最后一位”,因为我们在判断完指数的二进制的某一位后,那一位不再有用,

所以我们使用 b >>= 1也就是位运算符「右移」来消除使用过的那一位。由于我们不想计算当前是哪一位,并且希望代码尽可能的简便高效,我们只能不计当前是多少位,

用之前说过的利用平方来求下一个b1~bn中的一个。(b=Σ(i=1~n) bi,Σ为求和,求b1+.2+...bn,我有点啰嗦,但能让更多人看懂)。

所以,为了简洁高效地完成任务,实际上我们把原来的 一个n进制的数转为十进制等于 "求和( i = 0~n-1 ) n ^ i * 第 i 位的数字”  变成了 “ 求和( i = 0~n-1 ) n ^ i ”。

例如,十进制数10的二进制数1010按照我们原来的方式是: 2^0 * 0 + 2^1 * 1 + 2^2 * 0 + 2^3 * 1,而在代码里是 2^0  + 2^1  + 2^2  + 2^3 

这就不可避免的造成了在某个数的二进制的某一位是0而造成本该是0的指数被我们计算成了别的数。所以,我们一定要在一开始加上二进制下最后一位是否为0的条件,如果不是0那么就可以把当前得到的结果乘到我们的最终结果变量上,如果是0则不能乘到最终结果变量上,但是a依然要平方,不能因为这一位数字的结果被忽略而不计下一位数字的结果应当按照我们之前的方法线性求出本当乘上的数字。(例如,按照我们之前1010的例子,2^0 * 0被我们忽略,但是如果上一位数字的结果被忽略就不考虑下一位的话,这一位的指数就是2^0*1了,与我们期待的结果不符。)

说了这么多,发现自己想说的其实可以精炼一下,把自己的思考过程部分隐去。但是转念一想,对于第一次听说线性筛的OIer或者别的学习者需要详细的描述,而我自己也不能保证一直记住快速幂的原理,权当整理了。

如果本篇博客有差错或者不恰当之处,请多多指正。

原文地址:https://www.cnblogs.com/dudujerry/p/10702504.html