【7】逆元学习笔记
前言
逆元是模运算中很常用的计算方式,非常重要,是很多数论的辅助算法。
定义
在模 的意义下找到一个数 使得 ,则称 是 在模 的意义下的逆元,记作 。
特别的,当 时,逆元不存在。
逆元的性质:
由于这一点性质,我们可以在模意义下进行一边取模一边计算的除法运算,将模意义下的运算扩充到了除法,是非常常用的数论基础算法。
求法
第一境界:扩展欧几里得
由
得
这样就转化成了裴蜀等式,扩展欧几里得算法 解决。
#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;
}
时间复杂度:
第二境界:费马小定理
费马小定理:对于两个正整数 ,若 ,则有如下关系式:
根据费马小定理,我们可以推出以下式子:
结合逆元的定义式,此时 的逆元为 。
#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;
}
时间复杂度:
第三境界:线性递推
首先,一定有下面递推式:
设 ,则有下面式子:
等式两边同时乘以 ,得到:
由逆元的性质得到:
所以综合上述三式得到:
移项得:
展开上式得:
这已经是逆元的递推式了。
又因为 ,得到 。所以如果我们从 开始递推,那么到 时, 已经计算过了,满足递推的条件。
#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;
}
时间复杂度(总体):
时间复杂度(均摊):
小 trick:线性求阶乘逆元
首先用扩展欧几里得或费马小定理求出最后一项阶乘的逆元,然后从后往前递推。记 为 的逆元,易得如下递推式:
因为 中必然包含 的逆元,所以乘以 之后刚好抵消。而此时的 不存在 了,就只剩下 到 ,就是 。这样就可以在 的时间内求出阶乘逆元。
例题
例题 :
如第三境界:线性递推,不多赘述。
#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;
}
例题 :
暴力通项分数相加,得到这个式子:
做出以下定义:
易得以下递推式:(可在 内进行预处理)
最后直接套用预处理的数据,可以在 的时间内计算出 。最后运用逆元求出结果。
注意读入数据时需要使用快读卡常。慢了 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;
}
例题 :
旁证:等比数列求和公式
式 :
两边同时乘以 得式 :
式 减去式 得:
化系数为 得:
对于任意的 (其中 为 的不同素因子),则有因子和公式:
观察可得内部的求和是一个等比数列,直接套用等比数列求和公式得:
可以在分解质因数的同时计算这个式子,同时用逆元处理分母部分。
注意,由于此题的模数 很小,导致逆元可能不存在。当 时,逆元不存在。但此时易得这个式子:
#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;
}
后记
这篇笔记的数学公式又多又长又难懂,看来逆元难的根本不是其本身,而是综合时能否灵活运用。好像所有理科都是这样的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探