c++ 逆元 小总结

之前学习了一些逆元的知识点,在此做一个小总结。

定义

当我们在计算一些算式时会发现,当我们计算 % 时,可以转换为(a%m)(b%m),但%m就不能这样转换。这个时候该怎么办呢?我们知道,所以我们就可以找到x使 (mod m),(当然,这里要求b和m互质,否则没有对应解,这里就不证明了)在这里x其实和 是等价的,这时x就是b在mod m意义下的逆元。下面讲解几种求法:

题目

P3811 【模板】模意义下的乘法逆元 - 洛谷

给定正整数 n,p,求 [1,n] 中所有整数在模 p 意义下的乘法逆元。

a 模 p 的乘法逆元定义为 ax≡1(modp) 的解。

求法

下面两个方法适用于求解单个逆元(当然解这道题肯定是超时的啦)。

exgcd方法

一种方法是利用扩展GCD来求。这里先从GCD讲起。

GCD

GCD,就是求两个数的最大公约数。一个一个质数的试太慢了,复杂度应该在O()。我们来优化一下:

设这两个数分别为a和b, 为 x,a mod b 为 y。那么就能得到:

假设d为a,b两个数的公约数,那么等式两边同除以d,a除以d为整数,b除以d也为整数,所以等式右边为整数,那么等式左边也是整数,所以d也是y的约数。所以a,b的公约数同时也是b,a mod b的公约数。

反过来,假设d为b,a mod b两个数的公约数,依旧设 为 x,a mod b 为 y。那么就能得到:

等式两边同除以d,b除以d为整数,y除以d也为整数,所以等式右边为整数,那么等式左边也是整数,所以d也是a的约数。所以b,a mod b的公约数同时也是a,b的公约数。

综上,我们就能得出:gcd(a,b)=gcd(b,a mod b),当a mod b为0时,就说明能整除了,那么此时的b就是最大公约数。

代码
cpp 复制代码
long long gcd(long long a,long long b){
    if(b==0){
        return a;
    }
    return gcd(b,a%b);
}

EXGCD

EXGCD,就是找到一对x,y使得 。下面是证明:

要使式子恒等,则

当b=0时,我们一般将x设为1,y则为0,然后递归求解。

代码
cpp 复制代码
#define ll long long
ll x,y;
ll exgcd(ll a,ll b){
	if(!b){
		x=1;
		y=0;
		return a;
	}
	ll d=exgcd(b,a%b);
	ll x1=x,y1=y;
	x=y1;
	y=x1-a/b*y1;
	return d;
}

如何求逆元?我们设ax+my=gcd(a,m)。因为a和m互质,所以gcd(a,m)=1,ax+my也等于1。等式两边同mod m,等式右边仍然为1,等式左边my mod m等于0,所以ax mod m等于1。如果要求的为正数解,那么就要调整一下。

exgcd方法代码

cpp 复制代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p,x,y;
ll exgcd(ll a,ll b){
	if(!b){
		x=1;
		y=0;
		return a;
	}
	ll d=exgcd(b,a%b);
	ll x1=x,y1=y;
	x=y1;
	y=x1-a/b*y1;
	return d;
}
int main(){
	scanf("%lld%lld",&n,&p);
	for(int i=1;i<=n;i++){
		ll ans=exgcd(i,p);
		printf("%lld\n",(x%p+p)%p);
	}
	return 0;
}

费马小定理+快速幂

这种方法适用于模数p是质数的情况。费马小定理如下:

设 𝑝 是素数.对于任意整数 𝑎 且 𝑝 ∤𝑎,都成立 𝑎^𝑝−1 ≡1 (mod 𝑝).

具体证明见费马小定理 & 欧拉定理 - OI Wiki

由此,可知

所以就有一个简单的答案:。这个数怎么算?快速幂就行啦。

费马小定理+快速幂代码

cpp 复制代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p;
ll quick(ll a,ll k){
	ll ans=1;
	while(k){
		if(k%2)ans=ans*a%p;
		a=a*a%p;
		k/=2;
	}
	return ans;
}
int main(){
	scanf("%lld%lld",&n,&p);
	for(int i=1;i<=n;i++){
		ll anss=quick(i,p-2);
		printf("%lld\n",anss);
	}
	return 0;
}

接下来两个方法适用于求解多个逆元。

正推

这是一种从1到n的方法。1的逆元是1.假设当前到了a,可得:

两边同模p,可得

两边同乘a的逆元和(p mod a)的逆元,可得(将a的逆元表示为

移项,得

这样,就可以通过前面的逆元来求出当前数的逆元。

正推代码

cpp 复制代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p,ans[3000500];
int main(){
	scanf("%lld%lld",&n,&p);
	ans[1]=1;
	printf("%lld\n",1);
	for(int i=2;i<=n;i++){
		ans[i]=(p-p/i)*ans[p%i]%p;
		printf("%lld\n",ans[i]);
	}
	return 0;
}

倒推

这种方法因为也会计算阶乘的逆元,适用于排列组合。

首先,先将从1到n的阶乘去模p的结果算出来,然后通过前面exgcd或费马小定理+快速幂的方法求解出n的阶乘的逆元,然后从后往前遍历。把n阶乘的逆元看做,那么n阶乘的逆元就是,所从就能求出n到1阶乘的逆元。最后,跟前面一样,从1到n计算i的逆元,就是

倒推代码

cpp 复制代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,p,fact[3000500],inv[3000500];
ll quick(ll a,ll k){
	ll ans=1;
	while(k){
		if(k%2)ans=ans*a%p;
		a=a*a%p;
		k/=2;
	}
	return ans;
}
int main(){
	scanf("%lld%lld",&n,&p);
	fact[0]=1;
	for(int i=1;i<=n;i++){
		fact[i]=fact[i-1]*i%p;
	}
	inv[n]=quick(fact[n],p-2);
	for(int i=n-1;i>=1;i--){
		inv[i]=inv[i+1]*(i+1)%p;
	}
	for(int i=1;i<=n;i++){
		ll ans=inv[i]*fact[i-1]%p;
		printf("%lld\n",ans);
	}
	
	return 0;
}

如果大家有其他想法的,可以补充。

相关推荐
BackCatK Chen2 小时前
第十五章 吃透C语言结构与数据形式:struct/union/typedef全解析
c语言·开发语言·数据结构·typedef·结构体·函数指针·联合体
瓦特what?2 小时前
插 入 排 序
开发语言·c++
『往事』&白驹过隙;2 小时前
C/C++中的格式化输出与输入snprintf&sscanf
linux·c语言·c++·笔记·学习·iot·系统调用
Je1lyfish2 小时前
CMU15-445 (2026 Spring) Project#1 - Buffer Pool Manager
linux·数据库·c++·后端·链表·课程设计·数据库架构
m0_531237172 小时前
C语言-初始化赋值,函数,变量的作用域与生命周期
c语言·开发语言
张3蜂2 小时前
Python venv 详解:为什么要用、怎么用、怎么用好
开发语言·python
zyeyeye2 小时前
自定义类型:结构体
c语言·开发语言·数据结构·c++·算法
火龙果研究院2 小时前
在CentOS上安装Python 3.13需要从源码编译
开发语言·python·centos
俩娃妈教编程2 小时前
2023 年 03 月 二级真题(1)--画三角形
c++·算法·双层循环