模意义下的乘法逆元

前言

奇妙的东西!

1. 定义

对于两个整数 \(a,p\),如果 \(x\) 满足

\[ax\equiv 1\ \ \ (\operatorname{mod}p) \]

则称 \(x\)\(a\) 在模 \(p\) 意义下的乘法逆元,记作 \(a^{-1}\)
为了便于理解,这里使用乘方的性质,即 \(a\times a^{-1}=a^{1+(-1)}=1\) 来表示逆元,注意区别两者!

2. 性质

定义函数 \(\operatorname{inv}(a)\) 表示在模某个数下存在的 \(a\) 的逆元,设模数为 \(p\)

  • 性质 2.1 在任意 \(p\)\(1\) 的逆元是 \(1\)
  • 性质 2.2 \(a\times \operatorname{inv}(a)\equiv 1\ \ \ (\operatorname{mod} p)\)
  • 性质 2.3 当且仅当 \(\operatorname{gcd}(a,p)=1\)\(a\) 的逆元存在
  • 性质 2.4 \(0\) 在取余任何数的情况下都没有逆元。
  • 性质 2.5 \(\operatorname{inv}(a)\) 是一个完全积性函数
    \(\operatorname{inv}(a) \times \operatorname{inv}(b)=\operatorname{inv}(ab)\ \ \ (a,b \in \mathbb{Z^+})\)
  • 性质 2.6 \(\dfrac{a}{b}\equiv a\times \operatorname{inv}(b)\ \ \ (\operatorname{mod}p)\)

3. 求单个逆元

3.1 快速幂求

引入费马小定理:

若 p 为素数,\(\gcd(a, p) = 1\),则 \(a^{p - 1} \equiv 1 \pmod{p}\)

则如果求解逆元时 \(a,p\) 满足 \(\gcd(a, p) = 1\),且 \(p\) 为素数,带入费马小定理可得:

\[ax\equiv a^{p - 1}\ \ \ (\operatorname{mod}p) \]

\[x\equiv a^{p - 2}\ \ \ (\operatorname{mod}p) \]

这样,我们要求解的 \(x\) 就可以通过快速幂求出。
代码不给出,因为快速幂的应用范围较小。

3.2 扩展欧几里得求

我们使用解一元线性同余方程的方法,把原式变成这样:

\[\begin{aligned} ax&=1+py\\ ax-py&=1\\ ax+p(-y)&=1 \end{aligned}\]

观察到它与扩展欧几里得的一般式子很接近:

\[ax+by=\operatorname{gcd}(a,b) \]

则如果要求解逆元,用扩展欧几里得只需满足 \(\operatorname{gcd}(a,p)=1\) 即可。
定义函数 \(\operatorname{inv}(a,p)\) 表示求解 \(a\) 在模 \(p\) 意义下的逆元,代码如下:

ll inv(ll a,ll p){
	ll d=exgcd(a,p,x,y);//调用 exgcd 求解
	//其中 x 为要求解的逆元值,y 没什么用 
	while(x<0){//调整逆元为最小正整数 
		x+=p/d;
	}
	x%=p/d;
	return x;//返回 
}

4.求多个逆元

4.1 线性求逆元

例题:洛谷P3811

给定 \(n,p\)\(1\sim n\) 中所有整数在模 \(p\) 意义下的乘法逆元。
强制要求时间复杂度为 \(O(n)\)

使用神奇的凑数法:
考虑递推,假设我们现在要求解 \(1\sim n\)\(i\) 的逆元。
\(k=\left \lfloor \frac{p}{i} \right \rfloor\)\(j=p \operatorname{mod}i\)
则很显然 \(p=ik+j\)
考虑一个显然的式子:

\[p\equiv 0\ \ \ (\operatorname{mod}p ) \]

代入 \(p\)

\[ik+j\equiv 0\ \ \ (\operatorname{mod}p ) \]

神奇的来了,我们把两边同时乘以 \(i^{-1}\times j^{-1}\)

\[\begin{aligned} ik+j&\equiv 0\ \ \ (\operatorname{mod}p )\\ ik\times(i^{-1}\times j^{-1})+j\times(i^{-1}\times j^{-1})&\equiv 0\ \ \ (\operatorname{mod}p )\\ i\times i^{-1}\times kj^{-1}+j\times j^{-1}\times i^{-1}&\equiv 0\ \ \ (\operatorname{mod}p )\\ 1\times kj^{-1}+1\times i^{-1}&\equiv 0\ \ \ (\operatorname{mod}p )\\ kj^{-1}+ i^{-1}&\equiv 0\ \ \ (\operatorname{mod}p )\\ i^{-1}&\equiv -kj^{-1}\ \ \ (\operatorname{mod}p )\\ \end{aligned}\]

在把 \(k,j\) 的定义代回去呢?

\[i^{-1} \equiv -\left \lfloor \frac{p}{i} \right \rfloor \left(p \operatorname{mod}i\right)^{-1}\ \ \ (\operatorname{mod}p ) \]

我们注意到 \(p \bmod i < i\),因此我们总是可以从以前的推出现在的逆元值。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll inv[(int)(3e6)+5]; 
int main(){
	
	ll n,p;
	cin>>n>>p;
	inv[1]=1;//性质 2.1:1 的逆元永远是 1 
	cout<<1<<'\n';
	for(ll i=2;i<=n;i++){
		inv[i]=-(p/i)*inv[p%i];//公式 
		while(inv[i]<0)inv[i]+=p;//由于可能减出负数,把它调整为最小正整数。
		inv[i]%=p;//取余保证为最小正整数
		cout<<inv[i]<<'\n';
	}
	
	return 0;
} 

4.2 线性求任意 \(n\) 个数的逆元

我们想到了逆元的完全积性(性质 2.5)。
首先求出这任意 \(n\) 个数的前缀积,记作 \(s_i\)
求出 \(s_n\) 的逆元,记作 \(sv_n\),此时由积性,\(sv_n\) 就是这任意 \(n\) 个数逆元的积。
那么,\(sv_{n-1}=a_{n}\times s_{n-1}\) (偷懒就不写同余式了qwq)
很好理解:第 \(n\) 个数和它的逆元抵消成了 \(1\),剩下的就是 \(n-1\) 个数的逆元的积。
用这样的方式可以把所有 \(sv_i\) 都求出来。
再次利用抵消的方法,第 \(i\) 个数的逆元就 \(s_{i-1} \times sv_i\)
又可以递推啦 /gz
计算过程中不要忘记随时取余!

int n;
cin>>n>>mod;
s[0]=1;
for(int i=1;i<=n;i++){
	a[i]=read();
	s[i]=(1ll*s[i-1]*a[i])%mod;//处理前缀积 
}

//使用扩展欧几里得求解 s[n] 的逆元 sv[n] 
ll d=exgcd(s[n],mod,x,y);
while(x<0){
	x=(x+mod/d)%mod;
}
x=x%(mod/d);
x+=mod/d;
sv[n]=x; 
	
	
for(int i=n-1;i>=1;i--){
	sv[i]=(1ll*sv[i+1]*a[i+1])%mod;
}//求解所有的 sv ,i 的初值不一样导致写法不同,但不影响。
 
for(int i=1;i<=n;i++){
	inv[i]=(1ll*s[i-1]*sv[i])%mod;
}//求解所有数的逆元 

5. 逆元有什么用?

逆元可以在取模的条件下把除法变成乘法而不损失精度
例题:洛谷P5431

给定 \(n,p,k\)\(n\) 个正整数 \(a_i\),你要输出的答案为:

\[\sum\limits_{i=1}^n\frac{k^i}{a_i} \]

答案对 \(p\) 取模。

式子好像非常简单,可以直接算吗?
不可以!因为 \(\dfrac{k^i}{a_i}\) 可能会出现浮点数,取模运算不支持浮点数。
注意到答案对 \(p\) 取模,由性质 2.6,我们仅需要计算:

\[\sum\limits_{i=1}^n k^i\times a_i^{-1} \]

再对 \(p\) 取模即可。
于是用我们上面的方法求解出每个数的逆元即可解决本题。

int main(){
	
	int n;
	cin>>n>>mod>>k;
	s[0]=1;
	f[0]=1;
	f[1]=k;
	for(int i=2;i<=n;i++){
		f[i]=(1ll*f[i-1]*k)%mod;//一些预处理,见下文 
	}
	
	//求解逆元部分,不再写了
	 
	ll anss=0;
	for(int i=1;i<=n;i++){
		anss=(anss+(1ll*f[i]*inv[i])%mod)%mod;
		/*在这里,如果使用快速幂求解 k^i 会被卡时间,因为快速幂会让求解时多出一个 log 的时间复杂度
		但是我们发现 i 是从 1 到 n 的,因此可以通过预处理算好 k^i 的值保存,直接调用即可。 
		预处理时递推即可
		*/ 
	} 
	cout<<anss;
	
	return 0;
}

迁移自洛谷

posted @ 2025-02-04 13:10  hm2ns  阅读(108)  评论(0)    收藏  举报