数论
1. 筛法
给定 \(n\),求 \(1 \sim n\) 之间所有的素数。
小学数学,每次把 \(2\) 的倍数标记为合数,再把下一个素数的倍数标记为合数,直到 \(n\) 为止,时间复杂度 \(\mathcal{O}(n\log n)\),代码如下:
void init_prime(int n) {
for(int i = 2; i <= n; i++) {
if(!vis[i]) {
p[++cnt] = i;
for(int j = 2; i * j <= n; j++) {
vis[i * j] = true;
}
}
}
}
上述算法的时间复杂度没有非常优秀,其主要原因还是因为每个素数会被多次筛去,那有没有一种算法能使每个数被且仅被筛 \(1\) 次呢?
有的,这种算法就叫做欧拉筛,又叫线性筛,其时间复杂度达到了 \(\mathcal{O}(n)\)。
void init_prime(int n) {
for(int i = 2; i <= n; i++) {
if(!vis[i]) p[++cnt] = i;
for(int j = 1; i * p[j] <= n; j++) {
vis[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
其中 if(i % p[j] == 0) break;
是保证每一个质数仅被筛一次的关键,因为若 \(i\bmod p_j=0\) 说明 \(i\) 之前已经被 \(p_j\) 筛过了,那么 \(i\) 乘上其他质数也一定会被 \(p_j\) 筛掉,就没有必要再筛一遍了,那么此时,\(p_j\) 就是 \(i\) 的最小质因子。
那我们为什么要学习线性筛呢?只是为了 \(\mathcal{O}(n)\) 筛选质数?
当然不是,线性筛还可以在 \(\mathcal{O}(n)\) 的时间复杂度内筛一些积性函数的值,其中积性函数的定义为:若 \(i\) 与 \(j\) 互质,那么 \(f(ij)=f(i)\times f(j)\)。
2. 欧拉筛求欧拉函数
我们用 \(\varphi(i)\) 表示比 \(i\) 小且与 \(i\) 互质的数的个数,求 \(1\sim n\) 的 \(\varphi\) 值。
首先很显然,当 \(i\) 为质数时,\(\varphi(i)=i-1\)。
近一步,我们令 \(n^\prime\) 为 \(\dfrac{n}{p}\),其中 \(p\) 为 \(n\) 的最小质因子,我们分两种情况讨论。
如果 \(n^\prime \bmod p_1=0\) ,此时 \(\varphi(n)=p_1\times \varphi(n^\prime)\)。
如果 \(n^\prime \bmod p_1\ne0\),根据积性函数的定义,\(\varphi(n)=\varphi(p_1)\times \varphi(n^\prime)\)。
所以只需要在 break 的时候判断一下更新 \(\varphi\) 值就好了。
void init_phi(int n) {
phi[1] = 1;
for(int i = 2; i <= n; i++) {
if(!vis[i]) p[++cnt] = i, phi[i] = i - 1;
for(int j = 1; i * p[j] <= n; j++) {
vis[i * p[j]] = true;
if(i % p[j] == 0) {
phi[i * p[j]] = phi[i] * p[j];
break;
} else {
phi[i * p[j]] = phi[i] * phi[p[j]];
}
}
}
}
3. 拓展欧几里得算法
给定一个同余方程 \(ax\equiv b\ (\bmod\ n)\),求 \(x\) 的一个可行整数解。
首先这个方程就等价于 \(ax+ny=b\),其中 \(x,y\) 均为未知数,这就相当于求解一个二元一次不定方程组。显然,当 \(\gcd(a,n)\nmid b\) 时方程组不一定有解。
我们先考虑方程 \(ax+ny=\gcd(a,n)\),设:
因为
所以
那么 \(x_1=y_2\),\(y_1=x_2-\left\lfloor\dfrac{a}{n}\right\rfloor\times y_2\),递归求解即可,时间复杂度 \(\mathcal{O}(\log n)\):
int Exgcd(int a, int n, int& x, int& y) {
if(n == 0) {
x = 1, y = 0;
return a;
}
int gcd = Exgcd(n, a % n, x, y), t = x;
x = y, y = t - (a / n) * y;
return gcd;
}
例题:P1516 青蛙的约会
求关于 \(k\) 的方程 \(x+km\equiv y+kn\ (\bmod L)\) 的最小正整数解。
我们把式子进行变形:
我们令 \(A=m-n,B=y-x\),原方程转换为:
容易发现这就是欧几里得算法最标准的形式,可是我们求出来的解只是其中一个特解,所以我们需要通过取模数 \(L\) 得到最小正整数解。
#include<iostream>
#define int long long
using namespace std;
int Exgcd(int a, int b, int& x, int& y) {
if(b == 0) {
x = 1, y = 0;
return a;
}
int ans = Exgcd(b, a % b, x, y);
int t = x;
x = y, y = t - a / b * y;
return ans;
}
signed main() {
int x, y, m, n, L;
scanf("%lld%lld%lld%lld%lld", &x, &y, &m, &n, &L);
int a = n - m, b = L, c = x - y;
if(a < 0) a = -a, c = -c;
x = 0, y = 0;
int ans = Exgcd(a, b, x, y);
if(c % ans != 0) return puts("Impossible") & 0;
else printf("%lld", (x * (c / ans) % (b / ans) + (b / ans)) % (b / ans));
return 0;
}
3. 乘法逆元
如果正整数 \(x\) 满足 \(ax\equiv 1\ (\bmod\ b)\),那么我们称 \(x\) 为 \(a\) 在模 \(b\) 意义下的乘法逆元,所以模意义下的除法相当于乘上其模意义下的乘法逆元,并且只有 \(b\) 是质数时 \(a\)才存在模 \(b\) 意义下的乘法逆元。
其实本质和 exgcd 一样,同样可以使用 exgcd 算法在 \(\log\) 的时间复杂度内求出一个数的乘法逆元:
int Exgcd(int a, int b, int& x, int& y) {
if(b == 0) {
x = 1, y = 0;
return a;
}
int ans = Exgcd(b, a % b, x, y);
int t = x;
x = y, y = t - a / b * y;
return ans;
}
调用 Exgcd(a, b, x, y)
即可求出 \(a\) 在模 \(b\) 意义下的乘法逆元。
当然,我们还可以用费马小定理求解:
费马小定理:\(a^{b-1}\equiv 1\ (\bmod\ b)\),那么 \(a\times a^{b-2}\equiv 1\ (\bmod\ b)\),所以 \(a\) 在模 \(b\) 意义下的乘法逆元就是 \(a^{b-2}\)。
4. 计算组合数
为方便阅读,下文使用 \(\dbinom{n}{m}\) 代替 \(C_n^m\)。
因为组合数有公式 \(\dbinom{n}{m}=\dfrac{n!}{m!(n-m)!}\),所以可以计算出 \(m!\) 和 \((n-m)!\) 的逆元快速计算:
int C(int n, int m) {
return fac[n] * inv[fac[m]] % mod * inv[fac[n - m]] % mod;
}
因为 \(\dbinom{n}{m}=\dbinom{n}{m - 1} + \dbinom{n - 1}{m - 1}\),所以可以递推求解,时间复杂度 \(\mathcal{O}(nm)\):
int cal(int n, int m) {
C[0][0] = 1;
for(int i = 1; i <= n; i++) {
C[i][0] = 1, C[i][i] = 1;
for(int j = 1; j < i; j++) {
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
}
}
return C[n][m];
}
Lucas 定理:\(\dbinom{n}{m}\bmod p=\dbinom{\left\lfloor n / p \right\rfloor}{\left\lfloor m / p \right\rfloor}\times\dbinom{n\bmod p}{m\bmod p}\bmod p\),可以递归求解,通常是在 \(p\) 较小,\(n,m\) 较大的情况下使用:
int C(int n, int m, int p) {
if(m > n) return 0;
return fac[n] * fpow(fac[m], p - 2, p) % p * fpow(fac[n - m], p - 2, p) % p;
}
int Lucas(int n, int m, int p) {
if(m == 0) return 1;
return (Lucas(n / p, m / p, p) * C(n % p, m % p, p)) % p;
}
5. 中国剩余定理
给定线性同余方程组:
求 \(x\) 的最小正整数解,其中 \(p_1,p_2,p_3,\cdots,p_n\) 两两互质。
可以使用中国剩余定理(CRT)求解,其核心思想是让其满足其中一个方程组,并且在其他模数意义下答案为 \(0\),把答案加起来就是最后的答案。
我们用方程
来演式中国剩余定理的计算过程:
首先考虑满足第一个方程,并且在模其他模数的意义下均为 \(0\),得到方程:
解得 \(x=140\)。
接下来考虑满足第二个方程,并且在模其他模数的意义下均为 \(0\),得到方程:
解得 \(x=63\)。
然后考虑满足第三个方程,并且在模其他模数的意义下均为 \(0\),得到方程:
解得 \(x=30\)。
最后把解得的三个 \(x\) 值加在一起,得到 \(233\),对模数乘积取模得到 \(233\bmod 3\times 5\times 7 = 23\),所以最小正整数解就是 \(23\)。
那么我们现在的问题就变成了,如何解出每个方程组的 \(x\)。
对于第 \(i\) 个方程,我们设 \(p_i^\prime=\dfrac{\prod\limits^{n}_{k=1}p_k}{p_i}\),那么可以按照如下方法构造出每个方程组的解 \(x\):
- 计算 \(p_i^\prime\) 在模 \(p_i\) 意义下的乘法逆元 \(y\)。显然,此时 \(y\times p_i^\prime\times a_i\equiv a_i\ (\bmod \ p_i)\),并且 \(y\times p_i^\prime\times a_i\equiv 0\ (\bmod \ p_i^\prime)\)。
所以每个方程组中的 \(x\) 就是 \(y\times p_i^\prime\times a_i\)。
int CRT(int n) {
long long ans = 1, res = 0;
for(int i = 1; i <= n; i++) ans *= b[i];
for(int i = 1; i <= n; i++) {
long long p = ans / b[i];
int x = 0, y = 0;
Exgcd(p, b[i], x, y);
res = (res + (a[i] * p * x) % ans) % ans;
}
return (res + ans) % ans;
}