最近在学习的过程中碰上这样的问题:如何求N*N?那还不简单,直接一个for呗!(=・ω・=) 不急不急,听我慢慢说来,求的是1000000000次方怎么办?什么?这么变态!!!,当然我们也可以一个一个求,时间复杂度为O(n),你有耐心,OJ没有啊。。。那要怎么办呢?我们自然可以想想更快的方法,今天学习的就是采用神器--二进制来快速求幂!
快速幂的目的就是快速求幂 ←_← || →_→ 废话。。。那么它是怎么实现的呢?
1.将指数化为2进制,设我们要求10^10,(10)10==(1010)2;
2.10^10==10^(1010)2==10^(1*2^3)*10^(0*2^2)*10^(1*2^1)*10^(0*2^0)==10^8*10^2;
3.不难看出,以上过程就是二进制有1的地方乘对应的次数,没有1的地方相当于乘1,在乘法中相当于没乘!而一个数再怎么大也不过是0,1组成(二进制),逢1就乘,是0就跳过,可以大大节省时间!比如10^10朴素算法要算10次,而快速幂只用算2次;(不考路基数运算的3次)后面会解释什么是基数,一般来说,指数越大,快速幂优势越明显。
4.为什么可以这么算呢?
首先应该明白,二进制就是逢二进一,每个高位都是低位的2倍,因此当放到指数位上2倍就是平方的关系,比如10^(1010)2,从高位1到低位1依次是10^8,10^2,这两个1差两位,即4倍,平方的平方倍:((2)^2)^2; 因此我们应该设一个基数,来计算相应二进制位上对应的应该是底数的多少次幂:还是以10^(1010)2为例,从右往左的二进制位依次对应:10^0,10^2,10^4,10^8;数字代表有多少个,如:(1010)2 代表(从右往左): 0个10^0, 1个10^2, 0个10^4, 1个10^8; 这样相当于有1就乘上,是0就忽略,而二进制只有0和1;所以代码实现就很简单了:
上个模板:
//计算a^b;
int power(int a,int b)
{
int ans = 1;
int base = a;
while(b)
{
if(b&1) ans *= base;//该位为1,应该乘对应基数;
base = base*base;//基数更新;
b >>= 1;//同b/=2;
}
return ans;
}
//计算(a^b)%c;
int power(int a,int b,int c)
{
int ans = 1;
int base = a % c;
while(b)
{
if(b & 1)
ans = (ans%c * base%c) % c;
base = (base%c * base%c) % c;
b >>= 1;
}
return ans;
}
关于这个模板,base是基数,无论该位是否要乘,base都应该变成其平方(因为每一位的基数不一样,高位基数是低位基数的两倍(放在指数上体现为平方)),&是按位与运算,判断该位是否为1(另:&1判断奇偶数要比%2更快), >>是右移运算符,移动一位相当于除以2;
-------------------------------------------------------华丽的分割线-------------------------------------------------------
5.我们上几个题目看看:
(1).NYOJ 102 -->次方求模
这题显然要算的数的结果很大,因此不能考虑朴素算法,我们用快速幂,当然也要用到一些其他的定理:
(a*b)%c=(a%c*b%c)%c; (同余定理);
上代码:
#include<stdio.h>
#include<string.h>
int main()
{
long long a,b,c,i;
int n;
scanf("%d",&n);
while(n--)
{
scanf("%lld%lld%lld",&a,&b,&c);
long long ans=1,temp=a%c;//使a<c;
while(b!=0)//快速幂;
{
if(b&1)//该二进制位为1;
ans=(ans%c*temp%c)%c;
temp=(temp%c*temp%c)%c;//每一项(如果存在)都是前一项的平方倍;
b/=2;//或b>>=1;
}
printf("%lld\n",ans);
}
return 0;
}
主要是注意一下同余定理,不能对结果直接取余(结果也很可能会超过数据的范围,因此直接算出来也不现实)
(2).NYOJ 46--> 最少乘法次数
这个题目其实很有意思,题意说每次相乘的结果都可以用,其实这就暗示了可以用递归,再结合递推,就可以出答案,但是!还有更妙的方法吗?当然有,那就是--快速幂!(〜 ̄△ ̄)〜 我把两个思路都讲一下:
(2).1. 其实就是分情况讨论,n是奇数,则算第n次方最快的方法是算到n-1次方,再乘一次就是n次方,即N(n)==N(n-1)+1;
为什么?因为偶数可以整除2,每次n都除以2,肯定比你其他方法算更快接近1 (1是递归终点) ;也就是N(n-1)==N((n-1)/2)+1;
之后再讨论N((n-1)/2)的奇偶性,反复直到n==1即可;当n是偶数,就是N(n)==N(n/2)+1;再判断n/2的奇偶性,反复直到n==1即可;
(2).2. 敲黑板!重点来了!怎么用快速幂算这题呢?首先我们用极端法来考虑,假设我们指数每一次都乘2,即平方,直到不能平方为止,这时我们称得到的数为最大转化数,称平方的次数为最大转化次数。
举个栗子:2^11的最小乘法次数是多少?
我们来算指数的最大转化次数:1 -->2 -->4 -->8 -->16 (16>11,因此最大转化数为8,最大转化次数为3),那我们知道3次就可以乘到2^8,那么要几次可以达到2^11呢?答案是5次,还需2次,我们可以把11拆成二进制来考虑:(11)10==(1011)2;其中最高位的2对应2^3==8;我们已经达到了,剩下两次分别是2^2和2^1,正好对应(1011)2除最高位以外的1!于是我们可得算法:先计算最高位的1对应的次数,再加上剩余位上的1的个数,即为最少乘法次数。
注:这题其实不是严格的快速幂,但是其要求的最少乘法次数原理很像快速幂(都是通过减少乘法次数达到最快求幂),因此可以使用类快速幂思想解题.
上代码:
递推:
#include<stdio.h>//递推;
int main()
{
int m,n;
scanf("%d",&m);
while(m--)
{
int count=0;
scanf("%d",&n);
if(n==1)
{
printf("0\n");
continue;
}
else
{
while(n!=1)//循环直到n==1,回归初始状态;
{
if(n&1) n--;//n为奇数,2^n=2*2^(n-1),即2^(n-1)的次数+1;
else n/=2;//n为偶数,2^n=2^(n-1)*2^(n-1),即2^(n-1)的次数+1;
count++;
}
printf("%d\n",count);
}
}
return 0;
}
类快速幂:
#include<stdio.h>//类似快速幂;
int main()
{
int m,n;
scanf("%d",&m);
while(m--)
{
int count=0,temp;
scanf("%d",&n);
if(n==1)
{
printf("0\n");
continue;
}
else
{
temp=n;
while(temp!=1)//最大转化次数;
{
temp/=2;
count++;
}
while(n!=1)//快速幂思想;
{
if(n&1) count++;//在最大转化数之下,何时需要加;
n>>=1;
}
printf("%d\n",count);
}
}
return 0;
}
这里需要说一下,循环结束条件n!=1,是为了不把最高位的1统计进去(最高位已经按位权统计过了)
-------------------------------------------------------华丽的分割线-------------------------------------------------------
总结一下,快速幂是一个非常有用的小工具,在很多地方都可以用到它,比如:费马小定理求乘法逆元的时候,应该要理解并能够实现快速幂,把它当成一个模块存在脑子里即取即用,数学的路,我还有很长要走啊 (-_-#)