文章目录
快速幂运算&取模乘法性质
相关知识
1、问题引入
输入x,n,p,如何计算xn mod p?
一种暴力的做法就是直接将n个x乘起来,最终mod p。理论上来说,这样做显然是可以的,但是很明显,这样做的话,程序要循环n次,也就是说它的时间复杂度是O(n),如果n非常大,就很可能会导致TLE。那有什么办法,可以提高运算效率呢?
2、快速幂思想
如上图所示,快速幂的本质就是:底数不断取2次幂,指数不断除2次幂,直到指数除到为1,计算完毕。
这样做有什么好处?我们说过,原先的n次幂运算,它的时间复杂度为O(n),而如果是快速幂运算,如上图所示,时间复杂度就被降到了O(log2n)。
对于一个快速幂只要循环100次的低时间复杂度的运算,对于暴力的数学幂运算,究竟要循环多少次?下图给出了答案。
1022亿次循环,远远比100次多。快速幂,强行将10的22亿次降到100次,这就是快速幂的魅力。
因此,在题目中出现幂运算的时候并且指数范围非常大的时候,我们应该要联想到快速幂,来以防TLE导致的罚时。
3、快速幂中的奇数幂特判
快速幂的思路其实很简单,就是底数不断取2次幂,指数不断除2次幂,直到指数除到为1,计算完毕。但是,这个过程中,如果指数的奇数的话,就没有办法除2次幂。对于整数的除,都是整除。例如求2的5次方,经过一次快速幂,它并不会变成4的2.5次方,而是4的2次方,因为4整除2等于2。跟实际还是差了一个4的0.5次方,也就是“原底数”。
每一次到奇数幂时,就会有这些原底数被漏乘,最终形成一群因为是奇数幂而漏乘的“底数群”。因此,我们需要对奇数次幂的情况进行特判,存储这些没乘上的底数群,最终return的时候将它们都乘上。
由上,引入一个每一次处理奇数次幂时,未被底数乘上的待乘底数群。这个底数群的初始值为1,当为奇数次幂时,这个未乘的底数群就以乘以目前的底数的形式进行更新。最后,这个未乘的底数群乘幂为1时不包含奇数幂时遗漏的底数的不完全结果,就是快速幂的最终结果,思路如下所示:
我们的思路可以进一步简化为:
4、快速幂背后的一些问题
快速幂思想固然好,但是,快速幂带来的问题也存在,即底数数字变大的速度也是指数爆炸的速度。
例如,我们要求999910000,那么快速幂的过程就是这样的:
短短4次循环内,底数就到了100次幂!而就算是再后面的循环,数据有多大可想而知。那有什么办法,可以使得我们的数据缩小呢?
一般题目中考虑到这些问题,都会让我们将最终结果取某一个大数的模。
因此都会有类似于这样一句话:“由于数字可能比较大,输出数字一律取XXXXXXX的模。”
或者只取“个位”之类的限制性话语(言外之意就是所有数据都对10取余)。
例如,在2021年湖南大学的ACM新生赛中,我们可以看到:
即便是这样,还是存在着一些问题。我们的思路是对最终输出数字取模,以控制最终输出数据的大小。但是在没达到最终数字之前,我们的数据就很可能已经爆了。以求99999910000为例:
那么,怎么在过程中怎么控制数据呢,换言之,怎么在确保最终输出结果不变的情况下,让过程中的运算数据变小呢?可能有的同学会想到,对每一层循环的结果取模,这个思路很好,我们后续也会用到,不过这还是不够的,即便是这样还是有可能爆掉。还是以上图为例,第一次循环是107,对它取109的模(998244353)还是不会变,那么第二次循环它还是来到了1023,还是爆数据了。显然,这种方法还不够完善。
快速幂中,应付数据范围问题的终极秘密武器,就是“取模运算的乘法法则”。
5、取模运算乘法法则的介绍
如上图所示,这是取模运算的乘法法则。可能有的同学会疑惑,这乘法法则和我们快速幂运算中的幂运算有什么关系呢?其实我们都知道,幂运算的本质还是乘运算,快速幂中的幂,永远都是二次幂的迭代,如下图所示:
刚才我们最开始想到的是,对每一层循环的结果取余,即如下图
现在,利用取模运算性质,这就变成了
有了快速幂控制时间,再加以取模性质控制大小,幂运算的两个最大困难,就被成功克服了。
6、快速幂运算&取模乘法性质の核心代码模板(求x的n次幂对p取模的结果)
// 求“x的n次幂并最终结果对p取模”的快速幂代码模板
typedef ll long long
ll quick_pow(ll x, ll n) //利用快速幂算法+取模运算性质,剧减数据范围与时间复杂度
{
ll lack=1; //lack代表每一次奇数幂时积累下的未乘的底数群,所以初始值为1
x=x%p; //先将底数mod p,缩小数据范围
while(n!=1)//当指数为1时,底数x就是最终结果,不过还没乘入奇数次幂时的底数群
{
if(n&1)lack=lack*(x%p); //n&1即n为奇数时,lack存储,由于奇数幂被整除,少乘的底数群
x=((x%p)*(x%p))%p; //利用取模乘法性质: (a*b)%p=((a%p)*(b%p))%p
n>>=1; //幂的二进制位向右移一位,即幂整除2,这样整除效率更高
}
return ((lack%p)*(x%p))%p; //奇数次幂时未乘的底数群,乘上只考虑偶数次幂时的底数结果,就是结果
}
相关题目
1、N 的 N 次幂的个位数
参考思路
看到只求个位数,其实就是暗示所有结果都mod10,那么就可能用到我们的乘法的取模运算法则;看到N的范围109,那么显然暴力幂的方式肯定不太行,而要用快速幂,并且还需要借助mod10这个先决条件先**把数据范围给降下来。**那么思路就有了:快速幂+取模乘法性质一条龙解题。
参考AC代码
#include<bits/stdc++.h>
using namespace std;
#typedef ll long long
ll Pow(ll x, ll n) //利用快速幂算法+取模运算性质,剧减数据范围与时间复杂度
{
int lack=1;
x=x%10;
while(n!=1)
{
if(n&1)lack=lack*x%10; //ans存储奇数次幂时,由于幂整除,少乘的那些原底数
x=(x%10)*(x%10);
n>>=1; //b>>=1用于对b整除2,相较于b/=2,效率更高
}
return lack*x%10;
}
int main()
{
int t;
cin>>t;
ll n;
while(t--)
{
cin>>n;
cout<<Pow(n,n)<<endl;
}
return 0;
}
2、求AB的最后三位整数
参考思路
类似于第一题,看到只求最后三位数,其实就是暗示所有结果都mod1000,那么就可能用到我们的乘法的取模运算法则;看到A,B的范围104,很可能暴力幂的方式还是不太行,继续用快速幂。
思路就又有了:还是快速幂+取模乘法性质一条龙解题。
参考AC代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll quick_pow(ll x,ll n){
ll lack=1,x=x%1000;
while(n!=1){
if(n&1)lack=((lack%1000)*(x%1000))%1000;
x=((x%1000)*(x%1000))%1000,n/=2;
}
return ((lack%1000)*(x%1000))%1000;
}
int main(){
ll a,b;
while(cin>>a>>b&&(a+b)){
cout<<quick_pow(a,b)<<endl;
}
return 0;
}
参考
《2021杭电ACM-LCY算法培训入门班》
有任何问题,可以在评论区留言~
祝各位码力飞进!!!谢谢~