原理
大数取模
对于OI题中经常要求的将结果mod p后输出,首先有以下的数学原理:
直观理解:这两个规律很简洁,看起来就像是取模运算具有分配的性质(当然不是严格的数学的分配律)。直观上理解为什么,可以取p=10理解。我们可以有以下的例子:
在这个式子中,其实我们不需要看两个数的十位是什么,因为十位上的数肯定是10的倍数,无论十位是多少,加起来取模之后十位对应的部分肯定是0.所以,最后的结果只是简单的个位相加再对10取模,也就是
严格证明:这也是简单的。只需要将a,b写成整除部分加余数部分的形式
再代入,就可以得到
也就是
乘法的证明是类似的,在此略过。
进一步,我们可以从加法、乘法扩展到减法和除法。对于减法,我们有
之所以后面会多出来一个+p,是因为我们不希望是一个负数,但是如果不加上p,它就有可能是一个负数。这一点可以从这个式子的证明当中明显地看到(a,b的形式与加法公式所设相同,不再重复):
我们知道,在mod p之后,相差p的整数是等价的,如,因为两个数对7的余数是相等的(即在mod意义下相等)。如此,根据N-Q的取值范围,我们可以将它加上p,从而保证其值为正,又能够保证它的值不变(mod意义下)。按照数学上的定义,同余关系是自然数集上的等价关系。整数集关于模p同余关系的商集与普通意义下的加法构成加法交换群。
逆元
再扩展到除法时,我们就需要使用到拟元,因为一般地来讲,
很容易举反例验证。这里略过。
但是,如果我们想办法找到一个
那么,就有
从而转换为乘法进行运算。现在,问题就转换为如何求inv(b).在此处,我们可以引入费马小定理:
定理(费马小定理):如果p是一个质数,而整数a不是p的倍数,则有a^(p-1)≡1(mod p)。
将左边写成的形式,即可知道
因此,只需要求出即可。由于这个幂次p-2可能很高,所以在计算这个值的时候,就需要用到快速幂。
快速幂
我们先不考虑快速幂中间带着取模的情况。先来考虑一个简单的例子:计算
按照朴素的想法,我们会拿一个ans=1来,然后对它乘3,做10次,答案就有了。这样要算10次。
我们也可以这样想:
这样做为什么更快呢?我们试一试。
先令ans=1,base=3.
将base平方,得到我们发现当中有这个成分,所以
再将base平方,得到我们发现里面没有这个成分,不对ans作操作。
再将base平方,得到我们发现当中有这个成分,所以
得到答案,算法结束。一共进行了5次运算,相比朴素做法要快得多。直观上理解,是因为我们利用了中间结果:即假如我们已经知道了的值,我们对它平方就可以得到,而不需要遍历.剩下来的问题就变成,如何把一个分解成为一系列中某几项的乘积的形式。而上面的例子已经说明了,借用二进制把指数b写成一系列2的幂次之和就可以解决这个问题。
实现与细节
没有模的快速幂
#include <stdio.h>
#define ll long long
//a^b
ll fastpow(ll a,ll b){
ll base=a;
ll ans=1;
while (b){
if (b&1) ans=ans*base;
base*=base;
b>>=1;
}
return ans;
}
int main(){
printf("%lld",fastpow(3,10));
}
代码逻辑和上述思路基本是一样的,就是一个简单的模拟,不写了(懒得写了)
有模的快速幂
既然需要用到快速幂来计算,那基本上是一个很大的幂次。也就是说,long long十有八九是装不下的,OI题也会要求对结果取模输出。根据我们的数学原理
我们在写题的时候可以理解成,在中间步骤先取模,与算出来总的结果再取模是一样的。而且,我们也需要在中间步骤取模,因为乘到一半半可能就溢出了,每算一步都取个模更合理,算量也更小。那么,我么就会有以下的代码:
#include <stdio.h>
#define ll long long
//(a^b)%p
ll fastpow(ll a,ll b,ll p){
ll base=a%p;
ll ans=1;
while (b){
if (b&1) ans=(ans*base)%p;
base=(base*base)%p;
b>>=1;
}
return ans;
}
int main(){
printf("%lld",fastpow(3,10,7));
}
这里有一些小细节。
if (b&1) ans=(ans*base)%p;
base=(base*base)%p;
这里有没有必要在括号里面对ans和base再取模呢?没有。因为while循环执行一遍之后,由于mod的关系,ans和base的值肯定小于p,ans*base和base*base的值不会超过这个值肯定是不会超过longlong的上限的,而且就算超过了在括号里再取模也没用,因为ans,base都小于p,再取模相当于没取。
最重要的是这里:
ll base=a%p;
为什么第一步就要a%p?因为a本身可以非常大。如果不做这一步的话,我们跟着程序走,进入第一次循环。当执行到
base=(base*base)%p;
此时如果base=a,将会导致base*base直接越界。这种东西是有的,举例:
Problem - 896C - Codeforceshttps://codeforces.com/problemset/problem/896/C也就是珂朵莉树出处的原题)
在第三个测试点的第158个数据处,就会出现这种问题。
(别问,问就是卡了好久
实在记不得的话反正就这么几行,肌肉记忆得了