O(n) 时间复杂度的解法
前言
在逛 OI WiKi 的时候发现了一个 O(n) 时间复杂度的求逆元方法.
于是想到这题 Y 总给出的解法需要循环 n 次每次需要用 O(logn) 的时间运行费马小定理求逆元. 所以思考了一下能不能使用这个线性求逆元的算法预处理一下逆元, 然后就能够将整体的时间复杂度优化为 O(n) 了.
运行时间对比
可以看到使用线性求逆元确实是快了.
尽管 Oi WiKi 已经有了一些证明, 但是我觉得还是说得不够清楚. 所以我尝试证明了一下这个算法. 下面来介绍它.
线性求逆元算法
要计算 1 到 p 所有数模 p (其中, p 没有模 p 意义下的逆元) 的逆元时有一种线性的做法.
首先我们所以可以构造出除数 i, 余数 j, 商 k
p=ki+j
则有
p = ki + j \equiv 0 (\mod p)
将两边同时乘上逆元 i^{-1} 和 j^{-1} 可得
kj^{-1} + i^{-1} \equiv 0 (\mod p)
所以有
i^{-1} \equiv -kj^{-1} (\mod p)
又因为 k = \lfloor \frac{p}{i} \rfloor, j = p \mod i, 所以代入可得
i^{-1} = (- \lfloor \frac{p}{i} \rfloor)(p \mod i)^{-1} \mod p
由于 j 小于 i, 所以 j^{-1} 依然先于 i^{-1} 计算. 所以可以得出递推公式 :
inv[i] = -(p / i) \cdot inv[p \ \% \ i] \ \% \ p
如果要防止计算出负值, 则可以给同余方程两边同时乘上 1 - i, 可得 :
i^{-1} (1 - i) \equiv (1 - i) (- \lfloor \frac{p}{i} \rfloor)(p \mod i)^{-1} (\mod p)
展开可得
i^{-1} - i^{-1}i\equiv (p - \lfloor \frac{p}{i} \rfloor)(p \mod i)^{-1} (\mod p)
i^{-1} - 0\equiv (p - \lfloor \frac{p}{i} \rfloor)(p \mod i)^{-1} (\mod p)
i^{-1} \equiv (p - \lfloor \frac{p}{i} \rfloor)(p \mod i)^{-1} (\mod p)
因此, 我们的递推公式可以写成
inv[i] = (p - p / i)*inv[p \% i] \% p
使用限制
- 只能求 1 到 p 内的数在模 p 意义下的逆元
线性求逆元实现代码
typedef long long LL;
// 获得 1 到 n 区间内的所有乘法逆元 (n 要小于 p, 且 p 为质数)
vector<int> getRangeModularMultInv(int n, int p) {
vector<int> inv(n + 1);
inv[1] = 1;
for (int i = 2; i <= n; ++i)
inv[i] = (LL)(p - p / i) * inv[p % i] % p;
return inv;
}
QA
Q : 为什么只能计算 1 到 p
A : 因为 inv[i] 由 inv[p \% i] 推导, 如果 i 大于 p, 那么 inv[i] = inv[p], 而 p 不存在模 p 意义下的逆元, 所以只能计算 1 到 p - 1
题解
注意到题目数据 1≤b≤a≤10^5 < MOD = 10^9 + 7, 所以可以使用线性求逆元算法
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
vector<int> getRangeModularMultInv(int n, int p) {
vector<int> inv(n + 1);
inv[1] = 1;
for (int i = 2; i <= n; ++i)
inv[i] = (LL)(p - p / i) * inv[p % i] % p;
return inv;
}
const int N = 100010, MOD = 1e9 + 7;
int fac[N], invFac[N];
void calcFac(int n, int m) {
auto invI = getRangeModularMultInv(n, m);
fac[0] = 1; invFac[0] = 1;
for (int i = 1; i < N; i++) {
fac[i] = (LL)fac[i - 1] * i % m;
invFac[i] = (LL)invFac[i - 1] * invI[i] % m;
}
}
int getC(int a, int b, int m) {
return (LL)fac[a] * invFac[a - b] % m * invFac[b] % m;
}
int main() {
int q; cin >> q;
calcFac(N, MOD);
while (q--) {
int a, b; cin >> a >> b;
cout << getC(a, b, MOD) << endl;
}
return 0;
}
结语
证明真快乐.
没学过同余这些东西, 就自己硬证的. 不知道过程有没有错误, 欢迎大佬们指正 :)
很有启发性
PS:模符号可以这么写
\pmod p
,渲染出的结果是\pmod p如果写成
\mod p
,渲染出的结果是\mod p用pmod自带括号且前面没有很大的空白
# 哈哈,我用了50ms
# 跪求代码
43ms
#include<iostream> using namespace std; typedef long long LL; const int N=1e5+10,mod=1e9+7; int fac[N],inf[N]; int t,a,b; int qmi(int a,int b) { int res=1; while(b) { if(b&1) res=(LL)res*a%mod; b>>=1; a=(LL)a*a%mod; } return res; } int main() { fac[0]=1; for(int i=1;i<=100000;i++) fac[i]=(LL)fac[i-1]*i%mod; inf[(int)1e5]=qmi(fac[(int)1e5],mod-2); for(int i=100000;i>=1;i--) inf[i-1]=(LL)inf[i]*i%mod; scanf("%d",&t); while(t--) { scanf("%d%d",&a,&b); printf("%lld\n",(LL)fac[a]*inf[b]%mod*inf[a-b]%mod); } return 0; }
感谢
倒着预处理只算了一遍逆元,会快很多
很不错的博客,但是好像有一个地方有点小问题,最后为了防止负数的出现,并不是乘(1-i), 文中i^{-1} * i变成了0,实际上应该是1,所以推不出下面的式子,个人感觉这里是因为 -p/i下取整可以看作 -k , 而 (p - p / i) = (p - k), 在模p的情况下为-k ,和-k在模p情况下同余,所以可以替换