【7】逆元学习笔记

前言

逆元是模运算中很常用的计算方式,非常重要,是很多数论的辅助算法。

定义

在模 m 的意义下找到一个数 x 使得 ax1(modm),则称 xa 在模 m 的意义下的逆元,记作 inv(a)

特别的,当 ma 时,逆元不存在

逆元的性质:

a/ba×inv(b)(modm)

由于这一点性质,我们可以在模意义下进行一边取模一边计算的除法运算,将模意义下的运算扩充到了除法,是非常常用的数论基础算法。

求法

第一境界:扩展欧几里得

ax1(modb)

ax+by=1

这样就转化成了裴蜀等式扩展欧几里得算法 解决。

#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(logn)

第二境界:费马小定理

费马小定理:对于两个正整数 x,p,若 gcd(x,p)=1,则有如下关系式:

xpx(modp)

根据费马小定理,我们可以推出以下式子:

xp11(modp)

x×xp21(modp)

结合逆元的定义式,此时 x 的逆元为 xp2

#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(logn)

第三境界:线性递推

首先,一定有下面递推式:

inv(1)1(modp)

k=pi,r=pmodi,则有下面式子:

k×i+rp0(modp)

等式两边同时乘以 inv(i),inv(r),得到:

k×i×inv(i)×inv(r)+r×inv(i)×inv(r)0(modp)

由逆元的性质得到:

i×inv(i)1(modp)

r×inv(r)1(modp)

所以综合上述三式得到:

k×inv(r)+inv(i)0(modp)

移项得:

inv(i)k×inv(r)(modp)

展开上式得:

inv(i)pi×inv(pmodi)(modp)

这已经是逆元的递推式了。

又因为 r=pmodi,得到 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]=inv[i+1]×(i+1)

因为 inv[i+1] 中必然包含 i+1 的逆元,所以乘以 i+1 之后刚好抵消。而此时的 inv[i+1] 不存在 i+1 了,就只剩下 1i,就是 inv[i+1]。这样就可以在 O(n) 的时间内求出阶乘逆元。

例题

例题 1

P3811 【模板】乘法逆元

如第三境界:线性递推,不多赘述。

#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

P5431 【模板】乘法逆元 2

暴力通项分数相加,得到这个式子:

s=i=1nj=1i1aj×j=i+1naj×ki

z=i=1nai

ans=sz

做出以下定义:

qi=j=1iaj,hi=j=inaj

易得以下递推式:(可在 O(n) 内进行预处理)

qi=qi1×ai

hi=hi+1×hi

最后直接套用预处理的数据,可以在 O(n) 的时间内计算出 s。最后运用逆元求出结果。

ans=i=1nqi1hi+1kiinv(qn)

注意读入数据时需要使用快读卡常。慢了 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

P1593 因子和

旁证:等比数列求和公式

1

S=i=0nki

两边同时乘以 k 得式 2

kS=i=1n+1ki

2 减去式 1 得:

(k1)S=kn+11

化系数为 1 得:

S=kn+11k1=i=0nki

对于任意的 n=p1k1×p2k2×p3k3×...×prkr(其中 p1k1,p2k2,p3k3,...prkrn 的不同素因子),则有因子和公式

i=1rj=0kipij

观察可得内部的求和是一个等比数列,直接套用等比数列求和公式得:

i=1rpiki+11pi1

可以在分解质因数的同时计算这个式子,同时用逆元处理分母部分。

注意,由于此题的模数 9901 很小,导致逆元可能不存在。当 9901pi1 时,逆元不存在。但此时易得这个式子:

j=0kipijj=0ki1jki+1(mod9901)

#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;
}

后记

这篇笔记的数学公式又多又长又难懂,看来逆元难的根本不是其本身,而是综合时能否灵活运用。好像所有理科都是这样的

posted @   w9095  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示