【详解】快速幂&龟速乘&快速乘

我相信进来看的人都会快速幂,对吧(和善的眼神)

如果不会。。。。那我们现在开始讲吧(要不然为什么叫详解2333

如果已经知道,就跳到下面去看吧~

1. 快速幂

1.0 快速幂的诞生——最初的思路

我们通常需要求解形如 ab mod c 的式子,当b比较小的时候,我们通常可以想到用循环乘来解决这个问题,就像这样:

long long a,a1,b,c;
cin>>a>>b>>c;
a1=a;
for(int i=1;i<=b;i++)
a*=a1,a%=c;
cout<<a;

显然这个的时间复杂度是O(b)的,在平时基本够用了。

不过在某些大型的数据结构题里面(比如无良的线段树 ),这样的计算随处可见,假设那个b大一点,比如1e7,1e8之类的,然后再重复上几百几千次……

比如这个例子:

23333 123456787654321 19260817

你可以试着拿刚才那个循环跑一下(微笑)

如果你把数据中的b换成123454321,也就只需要跑2.8s而已(微笑x2)

所以,正是为了解决这样的问题,快速幂出现了。

1.1 快速幂的根据——二进制

没错,快速幂和我们的老朋友二进制又扯上关系了。

其主要原因是因为二进制一个很重要的性质,就是当十进制转化为二进制,会出现很有趣的地方。

我们来看这两个例子:

11= 8+2+1 =23+21+20
34= 32+2 =25+21

如你所见,它们可以拆成很多个2n的和。这是废话
我们再来看这两个例子:

311=38 × \times 32 × \times 31
1334=1332 × \times 132

有没有嗅到一丝危险的气息 发现什么有趣的东西?

1.2 快速幂的实现——位运算

如果你已经理解到它与二进制的关系,那么接下来的代码就很容易懂了:

//计算 (x^y)%mod
long long quick_pow(long long x,long long y,long long mod)
{
	long long sum=1;
    while(y!=0){
         if(y&1==1)sum=sum*x%mod;
    	     x=x*x%mod;
         	y=y>>1;
    }
}

(其实从我个人角度来说,一般在初学某个知识点时看到模板里面有位运算会感到头疼。不过,跟二进制相关的一些东西需要例外,因为这样反倒会更加容易理解)

代码看不懂?没关系,我们举个例子就好了,就用上面的311来说:

  1. 开始前:x=3 y=(1011)2=11;
  2. 第一轮:11&1=1,sum=3, x=9, y=(1011>>1)2=(101)2=5;
  3. 第二轮:5&1=1,sum=27, x=81,y=(101>>1)2=(10)2=2;
  4. 第三轮:2&1=0,sum=27,x=6561,y=(10>>1)2=(1)2=1;
  5. 第四轮:1&1=1,sum=177147,x=65612,y=(1>>1)2=(0)2=0;
  6. 循环结束,退出,得到sum=311=177147.

看到了吗?原本循环11次的算法被优化成了循环五次,而且由于与二进制相关联,指数b每一次减少一半,也就是说这个算法的时间复杂度是 O(logN)!log级别的算法代表什么,我不说大家也清楚~

所以,刚才的那个例子你可以拿来试试,博主不懂怎么测程序运行时间,只会加文件输入输出。 在本机上跑只花了0.57s,比起刚才的时间明显是大大优化了。

1.3快速幂总结——二进制大法好

二进制,在我们学习的道路上一直起着很大的用处,无论是加快运行的速度的位运算,或是快速处理前缀和的树状数组,还是今天提到的快速幂,都灵活运用了二进制的特性,从而大大优化了我们处理某些问题时浪费的时间和空间。

没错,快速幂的核心思想就是通过二进制的特有属性,先把次数转化为二进制,然后把那些为1的位(有贡献的)存下来,把为0的位(无贡献的)丢弃。我们之所以要进行这样的循环,就是为了依次处理出那些为1的位,然后累乘到答案里面。

实际上,快速幂的应用范围不止求ab mod c这一种,矩阵快速幂就是它的一个变式,有兴趣的同学可以去了解一下,在处理很多数学问题时非常有用。
(而且似乎也是考点之一??)

那么,了解了快速幂的这些思想,我们就要开始挑刺思考它被hack的可能性了。

2.龟速乘

2.0 龟速乘的诞生——快速幂的BUG

假设给你一个ab mod c的式子,现在的你已经可以说:我能够在log b的时间里算出来!

了吗?(和善的眼神x2)

你确定吗?(和善的眼神x3)

来来来,算一算这个,看看你的快速幂还活着不:

19260817 2333333 1234567654321

(我会说我只是把数字换了下顺序吗)

很好,现在我们找到了快速幂的一个大问题:当模数>1e9的时候,光是在相乘的时候就已经爆long long了。(当然你可以用__int128,不过NOIP似乎不允许

那怎么办?我们不能用快速幂了吗?

事实上想用还是可以的,不过看看标题,你就知道为了适应新的数据范围,我们需要付出什么代价了……

2.1 龟速乘的根据&实现——慢工出细活

比起计算机自带的乘法,龟速乘的的运行速度还要慢上一些。

但是,它可以有效地保证你的long long不会boom的一声炸掉,然后送给你一个神奇的数字。

代码奉上:

long long quick_mul(long long x,long long y,long long mod) 
{
	long long ans=0;
	while(y!=0){
		if(y&1==1)ans+=x,ans%=mod;
		x=x+x,x%=mod;
		y>>=1; 
	}
	return ans;
}

long long quick_pow(long long x,long long y,long long mod)
{
	long long sum=1;
    while(y!=0){
         if(y&1==1)sum=quick_mul(sum,x,mod),sum%=mod;
    	     x=quick_mul(x,x,mod),x%=mod;
         	 y=y>>1;
    }
    return sum;
}

相信你一眼就能看出来,这两个东西长的不是一般的像。

如果再仔细观察一下就会发现,快速幂里的x是指数级增长,而龟速乘变成了翻倍,仅此而已。

2.2 龟速乘总结——快速幂的补充

实际上,龟速乘的确慢,甚至比直接用开始提到的循环乘法还要慢(因为龟速乘相当于一个自行取模的乘号),然而慢工出细活,正是它的慢最终为我们解决了数据过大时产生的问题。

归根结底,龟速乘的出发点就是为了解决弥补快速幂的BUG,因而其思想与快速幂也十分接近。用一点点时间换来数据范围的扩大,想来是个不亏的交易。

当然,平时不担心爆long long的情况下,就没必要把龟速乘加上了~

讲了这么多,我们再看看最上面的标题,
貌似还有个压轴的东西没有讲……

3.快速乘

3.0快速乘的诞生—— 更精练,更简短

人们在研发出上面龟速乘以后,再也不用为了次方运算+取模的题型烦恼了……

然而,总是有人不满足:

就为了这种小问题,要我再多背那么长的模板?我才不干!

以上纯属yy

真相到底是什么我们无从知晓,但我们清楚的是结果:

那就是复杂度为O(1)的,一行可以打完的,快速乘的诞生。

3.1快速乘的根据——long double和long long的转换

这一部分也是刚刚学到,所以我只能找网上的资料了:

这是2009国家集训队论文:
骆可强:《论程序底层优化的一些方法与技巧》 (%%%)
在这里插入图片描述
说白了,其实我们就是在原地爆炸的边缘疯狂试探,把本来存进long long会炸掉的值先进行计算,用long double暂时存下,然后再把差值——一个不会超过long long的数字塞回去,再特判一下精度问题,就大功告成了。

所以,在压行之后就变成了这样:

        cin>>a>>b>>mod;
    	cout<<((a*b-(long long)((long double)a*b/mod)*mod+mod)%mod);

简洁明了,速度O(1),不过数值过大时容易造成误差,毕竟long double转换时会有精度上的误差,所以在真正要用的时候,还是建议大家使用龟速乘吧

(不过听说机房大佬dzyo用过几次都没什么事,emmm……)

至此,三种与幂运算相关的算法就全部讲完啦~

(P.S:博主第一篇讲解贴,若有不足敬请指出!顺便谢谢各位观看!)

猜你喜欢

转载自blog.csdn.net/Cyan_rose/article/details/83065026
今日推荐