模意义下的乘法逆元
前言
奇妙的东西!
1. 定义
对于两个整数 \(a,p\),如果 \(x\) 满足
则称 \(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\) 为素数,带入费马小定理可得:
这样,我们要求解的 \(x\) 就可以通过快速幂求出。
代码不给出,因为快速幂的应用范围较小。
3.2 扩展欧几里得求
我们使用解一元线性同余方程的方法,把原式变成这样:
观察到它与扩展欧几里得的一般式子很接近:
则如果要求解逆元,用扩展欧几里得只需满足 \(\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\):
神奇的来了,我们把两边同时乘以 \(i^{-1}\times j^{-1}\)
在把 \(k,j\) 的定义代回去呢?
我们注意到 \(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,我们仅需要计算:
再对 \(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;
}
迁移自洛谷