乘法逆元
逆元
例如一个数 \(a\),将 \(a\) 乘上 \(b\),会对 \(a\) 产生一部分的贡献,我们可以通过乘上某个数 \(x\) 来消除对 \(a\) 的贡献,那么 \(x\) 就是逆元。
乘法逆元
对于一个线性同余方程 \(ax \equiv 1 \pmod{b}\),我们称 \(x\) 为 \(a\) 在 \(\bmod\ b\) 意义下的逆元,也可以记为 \(a^{-1}\).
求解逆元
扩展欧几里得(exgcd)
扩展欧几里得是用来求 \(ax+by=c\) 的 \((x,y)\) 的整数解的。
而这个方程存在解的充分必要条件是 \(\gcd(a,b) \mid c\).
而扩展欧几里得算法是在欧几里得算法上延伸而来的,需要先去了解一下欧几里得算法的证明过程。
因为 \(\gcd(a,b) \mid a,b,c\),所以我们可以将 \(a,b,c\) 同除 \(\gcd(a,b)\),即原方程可化为 \(\frac{ax}{\gcd(a,b)}+\frac{by}{\gcd(a,b)}=\frac{c}{\gcd(a,b)}\),所以我们可以先求解 \(ax+by=1\) 且 \(a \perp b\) 的情况下 \((x,y)\) 的解。
那么可以得到:
那么就可以得到 \(x\) 变成了 \(y\),而 \(y\) 变成了 \(x- \lfloor\frac{a}{b} \rfloor y\),所以我们可以利用递归来求解 \((x,y)\).
很明显,当 \(b=0\) 时(即欧几里得算法的边界条件),\(a=1\),那么可以得到 \(x=1,y=0\).
而将 \((x,y)\) 乘上 \(c\) 就是 \(ax+by=c\) 的解。
如果要求最小非负数解,我们可以将 \(x=(x \bmod b+b)\bmod b\).
而对于这个最小非负数解涉及什么不定方程解系扩展,我太菜了没看懂qwq,求教。
void exgcd(int a,int b,int &x,int &y) {
if(!b) {
x=1;
y=0;
return;
}
exgcd(b,a%b,y,x);
y=y-a/b*x;
}
时间复杂度是 \(O(\log n)\).
快速幂求解(费马小定理)
用快速幂求解逆元实际上是运用了费马小定理。
费马小定理的定义是,若 \(p\) 为素数,且 \(\gcd(a,p)=1\),则 \(a^{p-1} \equiv 1 \pmod{p}\).
我们可以先取一个素数 \(p\),然后取一个不为 \(p\) 的倍数的数 \(a\),即满足 \(\gcd(a,p)=1\),那么我们可以构造一个序列 \(A=\{1,2,3,\dots,p-1\}\),易得 \(\forall i,\gcd(A_i,p)=1\),又因为 \(\gcd(a,p)=1\),所以 \(\gcd(A_i \times a,p)=1\),那么就有 \(\prod_{i=1}^{n}A_i \equiv \prod_{i=1}^{n}(A_i \times a) \pmod{p}\).
因为每一个 \(A_i\) 都对应了一个 \(A_i \times a\),所以设 \(f=(p-1)!\),又因为 \(p\) 为素数,所以很容易得出 \(\gcd(f,p)=1\),所以 \(f \equiv \prod_{i=1}^{p-1}(A_i \times a) \pmod{p}\),我们将这个同余式两边同乘 \(a^{p-1}\),可得 \(a^{p-1} \times f \equiv \prod_{i=1}^{p-1}A_i\),又因为 \(A=\{1,2,3,\dots,p-1\}\),所以 \(f=\prod_{i=1}^{p-1}A_i\),所以 \(a^{p-1} \equiv 1 \pmod{p}\).
那么我们回到逆元来,因为 \(ax \equiv 1 \pmod{b}\),所以根据费马小定理我们可知 \(a^{b-1} \equiv 1 \pmod{b}\),那么可得 \(ax \equiv a^{b-1} \pmod{b}\),此处是根据同余的性质得到的。就可以得到 \(x \equiv a^{b-2} \pmod{b}\),那么 \(a\) 在 \(\bmod b\) 意义下的逆元就是 \(a^{b-2}\)。
根据费马小定理,此处我们应保证 \(b\) 为素数且 \(b \nmid a\).
int quickly_pow(int a,int b) {
int res=1;
int mod=b;
a%=mod;
while(b) {
if(b&1) res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int navie(int a,int b) {
return quickly_pow(a,b-2);
}
时间复杂度是 \(O(\log n)\).
递推式求逆元
如果我们要求 \(1,2,3,\dots,n\) 的逆元,那么运用上面两种方法的时间复杂度均为 \(O(n \log n)\),此时如果需要一种 \(O(n)\) 的算法,就可以利用递推式来求逆元。
很显然,\(1^{-1} \equiv 1 \pmod{b}\).
那么我们可以设 \(b=k \times i+r\),那么我们就可以得到 \(k \times i+r \equiv 0 \pmod{b}\).
那么我们乘上 \(i^{-1} \times r^{-1}\) 可以得到 \(k \times r^{-1} + i^{-1} \equiv 0 \pmod{b}\),即 \(i^{-1} \equiv -k \times r^{-1} \pmod{b}\).
即 \(i^{-1} \equiv -\lfloor \frac{b}{i}\rfloor \times (b \bmod i)^{-1} \pmod{b}\).
又因为 \(b \bmod i < i\),所以我们可以得到求逆元的递推式:
void navie() {
inv[1]=1;
for(int i=2;i<=n;i++) inv[i]=(mod-mod/i)+inv[i-1]%mod;
//因为在模mod的意义下进行计算,所以-mod/i=mod-mod/i
}
线性求逆元
首先我们计算这要求逆元的 \(n\) 个数的积,我们记为 \(p_n\),然后我们可以用快速幂或扩展欧几里得计算出 \(p_n\) 的逆元,记为 \(inv_n\)。
很容易得到,当我们将 \(inv_p\) 乘上 \(a_i\) 时,就和 \(a_i\) 的逆元相抵消了,那么这样我们就可以从后往前求出第一个数到前一个数的积的逆元,即 \(inv_{n-1}=inv_n \times a_{n-1}\).
那么我们就可以用 \(inv_n \times p_n\) 来求出第 \(n\) 个数的逆元。
时间复杂度 \(O(n+\log p)\).
void navie() {
int p[N],inv_p[N];
int a[N],inv[N];
for(int i=1;i<=n;i++) p[i]=p[i-1]*a[i]%mod;
inv_p[n]=quickly_pow(p[n],mod-2);
//也可以用exgcd来求解
for(int i=n;i>=1;i--) inv_p[i-1]=inv_p[i]*a[i]%mod;
for(int i=1;i<=n;i++) inv[i]=inv_p[i]*p[i-1]%mod;
}
后记
对于扩展欧几里得算法,只需保证 \(\gcd(a,b)=1\),而对于根据费马小定理用快速幂求解,还需保证 \(b\) 为素数。
对于运用递推式和线性求法,则空间复杂度更高。

浙公网安备 33010602011771号