算法竞赛中常用的数学知识(持续更新ing)
(本博客不给出详细证明过程。)
- 数论
- 质数
- 约数
- 同余
- 其他
- 线性代数
- 矩阵
- 消元
- 线性变换
- 线性空间
- 组合数学
- 组合计数
- 容斥原理
- 相关的重要数列、函数
- 概率论
- 概率
- 数学期望
- 01分数规划模型
- 博弈论
- 博弈的基本概念
- SG函数的应用
质数
质数的判定
试除法 O(√N)
利用性质:若一个正整数 N 为合数,则存在一个能整除N的数T,其中2≤T≤√N。
根据这一性质,我们只需要依次检查 2~√N 之间的所有整数是否能整除N。试除法的时间复杂度是O(√N)。
// 试除法判定质数
bool ispr(int n) {
if (n < 2) return 0; // 特判0和1这两个整数,它们既不是质数也不是合数
for (int i = 2; i <= n/i; ++i) // 从2扫描到√N
if (n%i == 0) return 0;
return 1;
}
质数的筛选
用筛法求出 1~N 中所有的质数。除了筛质数以外,埃筛的思想可以推广到很多场景中去,挺有用的。
先来看第一种筛法——埃氏筛法 O(N log log N)
这个筛法的原理很简单:任意一个整数 x 的倍数 2x, 3x, ... 都不是质数。做法就是从小到大将每一个数的倍数标记为合数。做完这一操作之后,我们就可以根据一个数是否被标记来判断它是否为质数(若其未被标记,则为质数)。
另外,我们可以发现,2 和 3 都会把 6 标记为合数。也就是说,实际上小于x²的x的倍数在扫描更小的数的时候就已经被标记过了。因此,我们可以对埃筛进行优化,对每个数只需从x²开始扫描即可。
// 用埃筛筛质数
int v[N];
void pr(int n) {
memset(v, 0, sizeof v); // 合数标记
for (int i = 2; i <= n; ++i) {
if (v[i]) continue;
cout << i << '\n'; // i是质数
for (int j = i; j <= n/i; ++j) v[i*j] = 1;
}
}
埃氏筛法的时间复杂度是。
第二种筛法叫线性筛法 O(N)
即使埃氏筛法优化了(从x²开始筛),有时候还是不够快,因为它会重复标记合数。这时线性筛的优点就凸显出来了。它的做法是:从大到小累计质因子,让每个数只有唯一的产生方式。举个例子:让 12 只能分解成 3 * 2 * 2(而在埃筛中12会被分解成 2 * 6, 3 * 4,这意味着 12 会被标记两次,显然这是比较费时的)。
我们用数组v记录每个数的最小质因子。具体做法是这样子:
- 依次考虑 2~N 中的每一个数i
- 若v[i] = i(最小质因数是它自己,当然是质数), 说明 i 是质数,把它存下来。
- 扫描不大于v[i]的每个质数p,令v[i * p] = p。也就是在i的基础上累积一个质因子p。因为p ≤ v[i],所以p就是合数 i * p 的最小质因子。
每个合数 i * p 只会被它的最小质因子 p 筛一次,所以时间复杂度为O(N)。
// 用线性筛筛质数
int v[N], pr[N], m;
void pr(int n) {
memset(v, 0, sizeof v); // 存最小质因子
m = 0; // 质数的数量
for (int i = 2; i <= n; ++i) {
if (v[i] == 0) { // i是质数
v[i] = i;
pr[++m] = i;
}
// 给当前的数i乘上一个质因子
for (int j = 1; j <= m; ++j) {
// i有比pr[j]更小的质因子,或者超出n的范围,跳出循环
if (pr[j] > v[i] || pr[j] > n/i) break;
// pr[j]是合数i*pr[j]的最小质因子
v[i*pr[j]] = pr[j];
}
}
for (int i = 1; i <= m; ++i) cout << pr[i] << '\n';
}
质因数分解
算数基本定理:任何一个大于1的正整数都能唯一分解为有限个质数的乘积,可写作:
其中 cᵢ 都是正整数,pᵢ 都是质数,且满足p₁ < p₂ < ... < pₘ。
试除法 O(√N)
结合判定质数的试除法和埃氏筛法,我们扫描 2~√N 中的每个数d,若d|N,则从 N 中除去d,同时累计除去的 d 的个数。
因为一个合数在被扫描到之前,它的因子一定已经被除去了,所以上述过程中能整除N的一定是质数。最终就得到了分解质因数的结果。
特别地,若2~√N中没有一个数能整数N,则N是质数,无需分解。
// 试除法分解质因数
int p[N], c[N], m;
void divide(int n) {
m = 0;
for (int i = 2; i <= n/i; ++i) {
if (n%i == 0) {
p[++m] = i, c[m] = 0;
while (n % i == 0) n /= i, c[m]++; // 把i除干净
}
}
if (n > 1)
p[++m] = n, c[m] = 1;
for (int i = 1; i <= m; ++i) // 输出结果
cout << p[i] << '^' << c[i] << '\n';
}
约数
算法基本定理的推论:
在算术基本定理中,若正整数N被唯一分解为
,其中cᵢ都是正整数,pᵢ都是质数,且满足p₁ < p₂ < ... < pₘ,则N的正约数集合可写作:
N的正约数个数为:
N的所有正约数的和为:
求N的正约数集合:试除法 O(√N),所以总共的时间复杂度是O(N√N)
因为约数总是成对出现的(除了完全平方数,√N是单独出现的),所以我们只需扫描1~√N即可。判断 d 是否整除N,若是,则 N/d 也整除N。
int fac[1600], m = 0;
for (int i = 1; i <= n/i; ++i) {
if (n%i == 0) {
fac[++m] = i;
if (i != n/i) fac[++m] = n/i;
}
}
for (int i = 1; i <= m; ++i) // 输出结果
cout << fac[i] << '\n';
试除法的推论:一个整数N的约数个数上界为2√N
求 1~N 每个数的正约数集合——倍数法 O(N log N)
若用“试除法”分别求 1~N 每个数的正约数集合,时间复杂度过高,为O(N√N)。我们可以反过来考虑,对于每个数d,1~N中以 d 为约数的数就是 d 的倍数 d, 2d, 3d, ... , ⌊N / d⌋ * d。
vector<int> fac[500010];
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n/i; ++j)
fac[i*j].push_back(i);
for (int i = 1; i <= n; ++i) { // 输出结果
for (int j = 0; j < fac[i].size(); ++j)
printf("%d ", fac[i][j]);
puts("");
}
时间复杂度为O(N + N/2 + N/3 + ... + N/N) = O(N log N)。
倍数法的推论:1~N 每个数的约数个数的总和大约为N log N。
最大公约数
定理:∀a, b ∈N, gcd(a, b) * lcm(a, b) = a * b
更相减损术:∀a, b ∈N, a ≥ b, 有 gcd(a, b) = gcd(b, a-b) = gcd(a, a-b)
∀a, b ∈N, a ≥ b, 有 gcd(2a, 2b) = 2gcd(a, b)
欧几里得算法:∀a, b ∈N, b ≠ 0, gcd(a, b) = gcd(b, a mod b)
int gcd(int a, int b) {
return b ? gcd(b, a%b) : a;
}
用欧几里得算法求最大公约数的时间复杂度为O(log (a+b))。欧几里得算法是最常用的求最大公约数的方法。但是由于高精度除法(取模)不容易实现,所以我们在需要做高精度运算时可以考虑用更相减损术代替。
互质与欧拉函数
注意区分 gcd(a, b, c) = 1 和 gcd(a, b) = gcd(a, c) = gcd(b, c) = 1。前者表意为 a, b, c 互质,而后者表意为 a, b, c 两两互质。
欧拉函数
1~N中与 N 互质的数的个数被成为欧拉函数,记为φ(N)。
若在算数基本定理中, , 则:
(其证明思想是容斥原理)
根据欧拉函数的计算式,我们只需分解质因数,即可顺便求出欧拉函数。
int euler(int n) {
int ans = n;
for (int i = 2; i <= n/i; ++i) {
if (n%i == 0) {
ans -= ans/i;
while (n%i == 0) n /= i;
}
}
if (n > 1) ans -= ans/n;
return ans;
}
性质1~2:
1. ∀n > 1, 1~n中与n互质的数的和为n * φ(N) / 2。
2. 若 a, b 互质,则φ(ab) = φ(a)φ(b)。
积性函数:如果当 a, b 互质时,有 ƒ(ab) = ƒ(a) * ƒ(b),那么称函数 ƒ 为积性函数。
性质3~6:
3. 若 ƒ 是积性函数,且在算术基本定理中
,则。
4. 设 p 为质数,若 p|n 且 p²|n,则φ(n) = φ(n / p) * p。
5. 设 p 为质数,若 p|n 但 p²∤n,则φ(n) = φ(n / p) * (p - 1)。
6.
(注意:性质4~6只是欧拉函数的性质,并非所有积性函数都满足。)
求出2~N中每个数的欧拉函数
利用埃筛,我们可以按照欧拉函数的计算式,在O(N log N)的时间内完成。
void euler(int n) {
for (int i = 2; i <= n; ++i) phi[i] = i;
for (int i = 2; i <= n; ++i)
if (phi[i] == i)
for (int j = i; j <= n; j += i)
phi[j] = phi[j]/i*(i-1);
}
我们可以进一步优化,利用线性筛的思想在O(N)的时间内快速推出2~N中每个数的欧拉函数。
利用上文提到的性质4和性质5,我们知道每个合数n只会被它的最小质因子p筛一次。我们恰好可以在此时判断这两个条件,从φ(n/p)递推到φ(n)。
int v[N], pr[N], phi[N], m;
void euler(int n) {
memset(v, 0, sizeof v); // 最小质因子
m = 0; // 质数数量
for (int i = 2; i <= n; ++i) {
if (v[i] == 0) { // i是质数
v[i] = i, pr[++m] = i;
phi[i] = i-1;
}
// 给当前的数i乘上一个质因子
for (int j = 1; j <= m; ++j) {
// i有比pr[j]更小的质因子,或者超出n的范围,跳出循环
if (pr[j] > v[i] || pr[j] > n/i) break;
// pr[j]是合数i*pr[j]的最小质因子
v[i*pr[j]] = pr[j];
phi[i*pr[j]] = phi[i] * (i%pr[j] ? pr[j]-1 : pr[j]);
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】