【7】逆元学习笔记
前言
逆元是模运算中很常用的计算方式,非常重要,是很多数论的辅助算法。
定义
在模 \(m\) 的意义下找到一个数 \(x\) 使得 \(ax\equiv 1\pmod m\),则称 \(x\) 是 \(a\) 在模 \(m\) 的意义下的逆元,记作 \(inv(a)\)。
特别的,当 \(m\mid a\) 时,逆元不存在。
逆元的性质:
由于这一点性质,我们可以在模意义下进行一边取模一边计算的除法运算,将模意义下的运算扩充到了除法,是非常常用的数论基础算法。
求法
第一境界:扩展欧几里得
由
得
这样就转化成了裴蜀等式,扩展欧几里得算法 解决。
#include <bits/stdc++.h>
using namespace std;
int n,a,b,x,y;
int exgcd(int a,int b,int &x,int &y)
{
if(b==0)
{
x=1;
y=0;
return a;
}
int r=exgcd(b,a%b,x,y);
int d=x;
x=y;
y=d-a/b*y;
return r;
}
int main()
{
scanf("%d%d",&a,&b);
int r=exgcd(a,b,x,y);
printf("%d\n",(x+b)%b);
return 0;
}
时间复杂度:\(O(\log n)\)
第二境界:费马小定理
费马小定理:对于两个正整数 \(x,p\),若 \(\gcd(x,p)=1\) 且 \(p\) 为质数,则有如下关系式:
根据费马小定理,我们可以推出以下式子:
结合逆元的定义式,此时 \(x\) 的逆元为 \(x^{p-2}\)。
#include <bits/stdc++.h>
using namespace std;
long long n,p;
long long fast_power(long long a,long long p,long long m)
{
long long x=a,ans=1;
while(p)
{
if(p%2==1)ans=ans*x%m;
p/=2;
x=x*x%m;
}
return ans;
}
int main()
{
scanf("%lld%lld",&n,&p);
printf("%lld\n",fast_power(n,p-2,p));
return 0;
}
时间复杂度:\(O(\log n)\)
第三境界:线性递推
首先,一定有下面递推式:
设 \(k=\lfloor \frac{p}{i}\rfloor,r=p\bmod i\),则有下面式子:
等式两边同时乘以 \(inv(i),inv(r)\),得到:
由逆元的性质得到:
所以综合上述三式得到:
移项得:
展开上式得:
这已经是逆元的递推式了。
又因为 \(r=p\bmod i\),得到 \(r<i\)。所以如果我们从 \(1\) 开始递推,那么到 \(i\) 时,\(r\) 已经计算过了,满足递推的条件。
#include <bits/stdc++.h>
using namespace std;
long long n,p,inv[3000010];
int main()
{
scanf("%lld%lld",&n,&p);
inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=((p-p/i)*inv[p%i])%p;
for(int i=1;i<=n;i++)
printf("%lld\n",inv[i]);
return 0;
}
时间复杂度(总体):\(O(n)\)
时间复杂度(均摊):\(O(1)\)
小 trick:线性求阶乘逆元
首先用扩展欧几里得或费马小定理求出最后一项阶乘的逆元,然后从后往前递推。记 \(inv[i]\) 为 \(i!\) 的逆元,易得如下递推式:
因为 \(inv[i+1]\) 中必然包含 \(i+1\) 的逆元,所以乘以 \(i+1\) 之后刚好抵消。而此时的 \(inv[i+1]\) 不存在 \(i+1\) 了,就只剩下 \(1\) 到 \(i\),就是 \(inv[i+1]\)。这样就可以在 \(O(n)\) 的时间内求出阶乘逆元。
例题
例题 \(1\) :
如第三境界:线性递推,不多赘述。
#include <bits/stdc++.h>
using namespace std;
long long n,p,inv[3000010];
int main()
{
scanf("%lld%lld",&n,&p);
inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=((p-p/i)*inv[p%i])%p;
for(int i=1;i<=n;i++)
printf("%lld\n",inv[i]);
return 0;
}
例题 \(2\) :
暴力通项分数相加,得到这个式子:
做出以下定义:
易得以下递推式:(可在 \(O(n)\) 内进行预处理)
最后直接套用预处理的数据,可以在 \(O(n)\) 的时间内计算出 \(s\)。最后运用逆元求出结果。
注意读入数据时需要使用快读卡常。慢了 1ms 的记录
#include <bits/stdc++.h>
using namespace std;
int n,p,k,a[5000010],q[5000010],h[5000010],ans=0;
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
long long power(long long a,long long p,long long m)
{
long long x=a,ans=1;
while(p)
{
if(p%2==1)ans=ans*x%m;
p/=2;
x=x*x%m;
}
return ans;
}
int main()
{
n=read();p=read();k=read();
for(int i=1;i<=n;i++)
a[i]=read();
q[0]=1;
for(int i=1;i<=n;i++)
q[i]=(long long)q[i-1]*a[i]%p;
h[n+1]=1;
for(int i=n;i>0;i--)
h[i]=(long long)h[i+1]*a[i]%p;
for(int i=n;i>0;i--)
ans=(long long)(ans+(long long)q[i-1]*h[i+1]%p)*k%p;
printf("%d",(long long)ans*power(q[n],p-2,p)%p);
return 0;
}
例题 \(3\) :
旁证:等比数列求和公式
式 \(1\):
两边同时乘以 \(k\) 得式 \(2\) :
式 \(2\) 减去式 \(1\) 得:
化系数为 \(1\) 得:
对于任意的 \(n=p_{1}^{k_{1}}\times p_{2}^{k_{2}}\times p_{3}^{k_{3}}\times ... \times p_{r}^{k_{r}}\)(其中 \(p_{1}^{k_{1}},p_{2}^{k_{2}},p_{3}^{k_{3}},...p_{r}^{k_{r}}\) 为 \(n\) 的不同素因子),则有因子和公式:
观察可得内部的求和是一个等比数列,直接套用等比数列求和公式得:
可以在分解质因数的同时计算这个式子,同时用逆元处理分母部分。
注意,由于此题的模数 \(9901\) 很小,导致逆元可能不存在。当 \(9901\mid p_i-1\) 时,逆元不存在。但此时易得这个式子:
#include <bits/stdc++.h>
using namespace std;
long long a,b,ans=1,mod=9901;
long long power(long long c,long long p,long long m)
{
long long x=c,ans=1;
while(p)
{
if(p%2==1)ans=ans*x%m;
p/=2;
x=x*x%m;
}
return ans;
}
long long mprime()
{
long long m=0,ans1=1;
for(long long i=2;i<=a;i++)
{
if(a%i!=0)continue;
m=0;
while(a%i==0)
{
m++;
a=a/i;
}
m*=b;
if((i-1)%mod!=0)ans1=(power(i,m+1,mod)-1+mod)%mod*power(i-1,mod-2,mod)%mod;
else ans1=(m+1)%mod;
ans=ans*ans1%mod;
}
return 0;
}
int main()
{
scanf("%lld%lld",&a,&b);
if(b==0||a==1){printf("1");return 0;}
mprime();
printf("%lld",ans);
return 0;
}
后记
这篇笔记的数学公式又多又长又难懂,看来逆元难的根本不是其本身,而是综合时能否灵活运用。好像所有理科都是这样的

浙公网安备 33010602011771号