一、乘法逆元
如线性同余方程 a x ≡ 1 ( m o d b ) ax\equiv 1\pmod b ax≡1(modb) 的解 x x x 称作 a a a 在模 b b b 下的逆元,记作 x = a − 1 x=a^{-1} x=a−1。
二、乘法逆元存在的条件
根据线性同余方程组解存在的条件,当且仅当 gcd ( a , b ) = 1 \gcd(a,b) =1 gcd(a,b)=1 时, a a a 在模 b b b 下存在逆元。
三、求解逆元
1. 扩展欧几里得算法
使用扩展欧几里得算法可以在
O
(
log
b
)
O(\log b)
O(logb) 的时间复杂度下求出单个数的逆元。
求解线性同余方程
a
x
≡
1
(
m
o
d
b
)
ax\equiv 1\pmod b
ax≡1(modb) 即可。
void exgcd(long long a, long long b, long long &x, long long &y){
if (!b){
x = 1, y = 0;
return ;
}
exgcd(b, a % b, y, x);
y -= (a / b) * x;
}
long long get_inv(long long a, long long p){
long long x, y;
exgcd(a, p, x, y);
if (x < 0) x += p;
return x;
}
2. 快速幂
使用快速幂可以在
O
(
log
n
)
O(\log n)
O(logn) 的时间复杂度下求出单个数的逆元(模数应为质数)。
当
b
b
b 为质数时,由费马小定理得
a
b
−
1
≡
1
(
m
o
d
b
)
a^{b-1}\equiv 1 \pmod b
ab−1≡1(modb) ,因此
a
×
a
b
−
2
≡
1
(
m
o
d
b
)
a \times a^{b-2} \equiv 1 \pmod b
a×ab−2≡1(modb) ,即
a
a
a 的逆元为
a
b
−
2
a^{b-2}
ab−2 。代码如下:
long long qPow(long long a, long long b){
long long ret = 1;
for (; b; b >>= 1){
if (b & 1) ret = (ret * a) % p;
a = (a * a) % p;
}
return ret;
}
3. 线性求逆元
使用线性求逆元的方法可以在
O
(
b
)
O(b)
O(b) 的时间复杂度下求出
1
1
1 ~
b
−
1
b-1
b−1 所有数在模
b
b
b 下的逆元。
从
1
1
1 到
b
−
1
b-1
b−1 递推。
①
1
1
1 的逆元为
1
1
1
② 设当前数为
i
i
i ,且
b
=
k
i
+
r
(
r
<
i
)
b=ki+r(r<i)
b=ki+r(r<i) ,则
b
m
o
d
i
=
r
b\bmod i = r
bmodi=r 。
在模
b
b
b 意义下:
k
i
+
r
≡
0
(
m
o
d
b
)
ki+r\equiv 0\pmod b
ki+r≡0(modb)
两边同时乘以
i
−
1
r
−
1
i^{-1}r^{-1}
i−1r−1 得:
k
r
−
1
+
i
−
1
≡
0
(
m
o
d
b
)
kr^{-1}+i^{-1}\equiv0\pmod b
kr−1+i−1≡0(modb)
移项整理:
i
−
1
≡
−
k
r
−
1
≡
−
⌊
b
i
⌋
(
b
m
o
d
i
)
−
1
(
m
o
d
b
)
i^{-1}\equiv -kr^{-1}\equiv -\lfloor\frac{b}{i}\rfloor (b\bmod i)^{-1}\pmod b
i−1≡−kr−1≡−⌊ib⌋(bmodi)−1(modb)
由于 b m o d i < i b\bmod i < i bmodi<i ,因此已知,可以 O ( 1 ) O(1) O(1) 递推。代码如下(Luogu 3811 【模板】 乘法逆元):
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e6 + 5;
int n, p;
int inv[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin >> 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 ++) cout << inv[i] << '\n';
return 0;
}
4. 线性求逆元(任意)
接下来介绍一种可以在几乎线性的时间内求出给出数列
{
a
n
}
\{a_n\}
{an} 中所有数在模
b
b
b 意义下的逆元的方法。
首先计算
{
a
i
}
\{a_i\}
{ai} 的前缀积,记为
{
s
u
m
n
}
\{sum_n\}
{sumn} ,即
s
u
m
i
=
Π
j
=
1
i
a
j
sum_i=\Pi_{j=1}^{i} a_j
sumi=Πj=1iaj 。其次计算
s
u
m
n
sum_n
sumn 的逆元,记为
i
n
v
s
n
invs_n
invsn 。将
i
n
v
s
n
invs_n
invsn 倒推求出每一个前缀积的逆,记作
{
i
n
v
s
n
}
\{invs_n\}
{invsn} ,递推公式为:
i
n
v
s
i
−
1
=
i
n
v
s
i
∗
a
i
invs_{i-1} = invs_{i} * a_i
invsi−1=invsi∗ai
有了两个数组,就可以据此得出所有数的逆元
{
i
n
v
i
}
\{inv_i\}
{invi} :
i
n
v
i
=
i
n
v
s
i
−
1
×
s
u
m
i
inv_i=invs_{i-1}\times sum_i
invi=invsi−1×sumi
代码如下(Luogu 5431 【模板】 乘法逆元2):
/*
提示:题目卡常,需要使用快读
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e6 + 5;
long long n, p, k;
long long a[maxn], sum[maxn], invs[maxn];
template <typename T>
inline void read(T &x){
char c;
x = 0;
int fu = 1;
c = getchar();
while(c > 57 || c < 48){
if(c == 45) fu = -1;
c = getchar();
}
while(c <= 57 && c >= 48){
x = (x << 3) + (x << 1) + c - 48;
c = getchar();
}
x *= fu;
}
long long qPow(long long a, long long b){
long long ret = 1;
for (; b; b >>= 1){
if (b & 1) ret = (ret * a) % p;
a = (a * a) % p;
}
return ret;
}
int main(){
read(n), read(p), read(k);
for (int i = 1; i <= n; i ++) read(a[i]);
sum[0] = 1;
for (int i = 1; i <= n; i ++) sum[i] = sum[i - 1] * a[i] % p;
invs[n] = qPow(sum[n], p - 2);
for (int i = n - 1; i >= 1; i --) invs[i] = invs[i + 1] * a[i + 1] % p;
long long mulk = 1, ans = 0;
for (int i = 1; i <= n; i ++){
mulk = mulk * k % p;
ans = (ans + mulk * (invs[i] * sum[i - 1] % p) % p) % p;
}
printf("%lld\n", ans);
return 0;
}