12 线性求逆元
学习目标
学完这一节,你应该能够:
- 理解为什么批量求逆元时需要更快的方法;
- 一步一步推出线性求逆元递推式;
- 理解为什么递推中出现的每一个逆元都存在;
- 理解为什么计算
inv[i] 时 inv[p % i] 已经算过;
- 独立写出线性求逆元代码。
1. 为什么需要批量求逆元
如果只求一个数的逆元,可以使用:
- 扩展欧几里得算法;
- 素数模下使用费马小定理加快速幂。
但如果要求:
1,2,3,…,n
在模 p 下的所有逆元,一个一个使用快速幂,大约需要:
O(nlogp)
当:
- p 是素数;
- 1≤n<p;
时,可以利用一个递推公式,把总复杂度降到:
O(n)
2. 我们要求什么
令:
inv[i]
表示 i 在模 p 下的逆元。
也就是说:
i⋅inv[i]≡1(modp)
首先:
1×1≡1(modp)
所以:
inv[1]=1
接下来考虑怎样从前面已经求出的逆元得到 inv[i]。
3. 从带余除法开始
因为:
i<p
所以把 p 除以 i:
p=qi+r
其中:
q=⌊ip⌋
并且:
r=pmodi
余数满足:
0≤r<i
当 i≥2 时,因为 p 是素数且 i<p,所以 i 不可能整除 p。
因此:
r=0
所以:
1≤r<i
4. 把带余除法变成同余式
从:
p=qi+r
得到:
qi+r=p
两边模 p。
因为:
p≡0(modp)
所以:
qi+r≡0(modp)
把 r 移到右边:
qi≡−r(modp)
到这里还没有做任何“除法”,只是使用同余的加减法性质。
5. 第一次乘逆元:消去 i
因为:
1≤i<p
且 p 是素数,所以:
gcd(i,p)=1
因此 i 在模 p 下存在逆元 i−1。
从:
qi≡−r(modp)
两边同时乘 i−1:
qi⋅i−1≡−r⋅i−1(modp)
因为:
ii−1≡1(modp)
所以:
q≡−ri−1(modp)
6. 第二次乘逆元:消去 r
前面已经证明:
1≤r<i<p
因为 p 是素数,所以:
gcd(r,p)=1
因此 r 也存在逆元 r−1。
从:
q≡−ri−1(modp)
两边同时乘 r−1:
qr−1≡−ri−1r−1(modp)
因为:
rr−1≡1(modp)
所以:
qr−1≡−i−1(modp)
两边同时乘 −1:
−qr−1≡i−1(modp)
因此:
i−1≡−qr−1(modp)
7. 换成程序中的写法
前面:
q=⌊ip⌋
所以程序中:
q = p / i;
并且:
r=pmodi
对应:
r = p % i;
因此:
inv[i]≡−⌊ip⌋⋅inv[pmodi](modp)
为了把负号改成非负的同余代表,可以利用:
−q≡p−q(modp)
得到:
inv[i]=(p−⌊ip⌋)⋅inv[pmodi]modp
程序写成:
inv[i] = (p - p / i) * inv[p % i] % p;
8. 为什么 inv[p % i] 一定已经算过
因为:
r=pmodi
并且:
1≤r<i
所以:
pmodi<i
程序按照:
1,2,3,…,n
从小到大计算。
当正在计算:
inv[i]
时,所有下标小于 i 的逆元都已经算完。
而:
pmodi<i
所以:
inv[pmodi]
一定已经有值。
这就是这个递推能够从小到大进行的关键。
9. 用模 7 的例子一步一步计算
设:
p=7
9.1 inv[1]
inv[1]=1
9.2 inv[2]
7=3×2+1
所以:
inv[2]=(7−3)⋅inv[1]mod7
得到:
inv[2]=4
检查:
2×4=8≡1(mod7)
9.3 inv[3]
7=2×3+1
所以:
inv[3]=(7−2)⋅inv[1]mod7
得到:
inv[3]=5
检查:
3×5=15≡1(mod7)
9.4 inv[4]
7=1×4+3
所以:
inv[4]=(7−1)⋅inv[3]mod7
得到:
inv[4]=6×5mod7=2
检查:
4×2=8≡1(mod7)
10. C++ 模板
const int N = 1000000;
long long inv[N + 1];
void init_inv(int n, long long p) {
inv[1] = 1;
for (int i = 2; i <= n; i++) {
inv[i] = (p - p / i) * inv[p % i] % p;
}
}
11. 为什么要求 p 是素数且 n<p
我们需要保证:
1,2,…,n
中的每个数在模 p 下都有逆元。
当 p 是素数,并且:
1≤i≤n<p
时:
p∤i
所以:
gcd(i,p)=1
每个 i 都存在逆元。
同时递推中的:
r=pmodi
也满足:
1≤r<i<p
所以 r 也有逆元。
12. 为什么时间复杂度是 O(n)
循环:
for (int i = 2; i <= n; i++)
执行大约 n 次。
每次只进行常数次:
所以总时间复杂度是:
O(n)
13. 小练习
练习 1
为什么线性求逆元通常要求 p 是素数并且 n<p?
练习 2
从:
qi≡−r(modp)
开始,一步一步推出:
i−1≡−qr−1(modp)
练习 3
为什么 inv[p % i] 一定在 inv[i] 之前算完?
14. 答案
练习 1
为了保证 1,2,…,n 全部与 p 互质,所以每一个逆元都存在。
练习 2
先两边乘 i−1:
q≡−ri−1(modp)
再两边乘 r−1:
qr−1≡−i−1(modp)
最后两边乘 −1:
i−1≡−qr−1(modp)
练习 3
因为:
pmodi<i
而程序按下标从小到大计算。
有帮助,赞一个