【算法】大数取模、逆元、快速幂的原理实现和细节

原理

大数取模

对于OI题中经常要求的将结果mod p后输出,首先有以下的数学原理:

(a+b)%p=(a%p+b%p)%p

(a*b)%p=(a%p*b%p)%p.

直观理解:这两个规律很简洁,看起来就像是取模运算具有分配的性质(当然不是严格的数学的分配律)。直观上理解为什么,可以取p=10理解。我们可以有以下的例子:

(11+23)%10=34%10=4.

在这个式子中,其实我们不需要看两个数的十位是什么,因为十位上的数肯定是10的倍数,无论十位是多少,加起来取模之后十位对应的部分肯定是0.所以,最后的结果只是简单的个位相加再对10取模,也就是

(1+3)%10=4.

严格证明:这也是简单的。只需要将a,b写成整除部分加余数部分的形式

a=Mp+N,b=Sp+Q(M,N,S,Q\in N^{+})

再代入,就可以得到

(a+b)%p=(Mp+N+Sp+Q)%p=((M+S)p+(N+Q))%p=(N+Q)%p.

也就是

(a+b)%p=(N+Q)%p=(a%p+b%p)%p.

乘法的证明是类似的,在此略过。

进一步,我们可以从加法、乘法扩展到减法和除法。对于减法,我们有

(a-b)%p=(a%p-b%p+p)%p.

之所以后面会多出来一个+p,是因为我们不希望a%p-b%p是一个负数,但是如果不加上p,它就有可能是一个负数。这一点可以从这个式子的证明当中明显地看到(a,b的形式与加法公式所设相同,不再重复):

(a-b)%p=(Mp+N-Sp-Q)%p=(N-Q)%p.

0\leqslant N\leqslant p-1,0\leqslant Q\leqslant p-1,

 \therefore N-Q\in[-p+1,p-1].

我们知道,在mod p之后,相差p的整数是等价的,如6=13(mod7),因为两个数对7的余数是相等的(即在mod意义下相等)。如此,根据N-Q的取值范围,我们可以将它加上p,从而保证其值为正,又能够保证它的值不变(mod意义下)。按照数学上的定义,同余关系是自然数集上的等价关系。整数集关于模p同余关系的商集与普通意义下的加法构成加法交换群。

逆元

再扩展到除法时,我们就需要使用到拟元,因为一般地来讲,

(\frac{a}{b})%p \neq \frac{a%p}{b%p}.

很容易举反例验证。这里略过。

但是,如果我们想办法找到一个

b* inv(b)=1(mod\ p).

那么,就有

(\frac{a}{b})%p=(a*inv(b))%p.

从而转换为乘法进行运算。现在,问题就转换为如何求inv(b).在此处,我们可以引入费马小定理

定理(费马小定理):如果p是一个质数,而整数a不是p的倍数,则有a^(p-1)≡1(mod p)。

将左边写成a*a^{p-2}的形式,即可知道

a^{p-2}=inv(a).

因此,只需要求出a^{p-2}即可。由于这个幂次p-2可能很高,所以在计算这个值的时候,就需要用到快速幂。

快速幂

我们先不考虑快速幂中间带着取模的情况。先来考虑一个简单的例子:计算3^{10}.

按照朴素的想法,我们会拿一个ans=1来,然后对它乘3,做10次,答案就有了。这样要算10次。

我们也可以这样想:

10=(1010)_{2}=8+2,

\therefore 3^{10}=3^{8}*3^{2}.

这样做为什么更快呢?我们试一试。

先令ans=1,base=3.

将base平方,得到3^{2}.我们发现3^{10}当中有这个成分,所以ans=ans*base.

再将base平方,得到3^{4}.我们发现3^{10}里面没有这个成分,不对ans作操作。

再将base平方,得到3^{8}.我们发现3^{10}当中有这个成分,所以ans=ans*base.

得到答案,算法结束。一共进行了5次运算,相比朴素做法要快得多。直观上理解,是因为我们利用了中间结果:即假如我们已经知道了a^{p}的值,我们对它平方就可以得到a^{2p},而不需要遍历a^{p+1},a^{p+2},....剩下来的问题就变成,如何把一个a^{b}分解成为一系列a^{p},a^{2p},a^{4p},...中某几项的乘积的形式。而上面的例子已经说明了,借用二进制把指数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题也会要求对结果取模输出。根据我们的数学原理

(a*b)%p=(a%p*b%p)%p.

我们在写题的时候可以理解成,在中间步骤先取模,与算出来总的结果再取模是一样的。而且,我们也需要在中间步骤取模,因为乘到一半半可能就溢出了,每算一步都取个模更合理,算量也更小。那么,我么就会有以下的代码:

#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的值不会超过p^{2}.这个值肯定是不会超过longlong的上限的,而且就算超过了在括号里再取模也没用,因为ans,base都小于p,再取模相当于没取。

最重要的是这里:

ll base=a%p;

为什么第一步就要a%p?因为a本身可以非常大。如果不做这一步的话,我们跟着程序走,进入第一次循环。当执行到

base=(base*base)%p;

此时如果base=a,将会导致base*base直接越界。这种东西是有的,举例:

Problem - 896C - Codeforcesicon-default.png?t=M3C8https://codeforces.com/problemset/problem/896/C也就是珂朵莉树出处的原题)

在第三个测试点的第158个数据处,就会出现这种问题。

(别问,问就是卡了好久

实在记不得的话反正就这么几行,肌肉记忆得了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值