数论
质数筛
埃氏筛
枚举出质数后,将它的倍数(即其倍数的合数)标记为合数,时间复杂度 \(\Theta(n\log\log n)\)。
实现
节选自 OI Wiki
vector<int> prime;
bool is_prime[N];
void Eratosthenes(int n) {
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; ++i) is_prime[i] = true;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
prime.push_back(i);
if ((long long)i * i > n) continue;
for (int j = i * i; j <= n; j += i)
// 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i
// 的倍数开始,提高了运行速度
is_prime[j] = false; // 是 i 的倍数的均不是素数
}
}
}
线性筛(欧拉筛)
容易发现埃氏筛会对相同合数多次标记,让每个合数只标记一次即可。
过程:
- 遍历 \(i\) 从 \(2\) 到 \(n\);
- 若 \(i\) 是质数,将 \(i\) 加入质数列表;
- 从小到大遍历质数列表,设当前质数为 \(p\):
- 筛掉 \(i\times p\);
- 若 \(i\bmod p=0\),则退出循环。
对于最后一次操作的证明:当 \(i\) 包含因子 \(p\) 时,\(p\) 是 \(i\) 的最小质因子,那么对于更大的质数 \(p^\prime\),\(i\times p^\prime\) 的最小质因子不是 \(p^\prime\) 而是 \(p\),不应该在这里筛掉,应在后面筛,保证每个合数只被其最小质因子筛一次。
时间复杂度 \(\Theta(n)\)。
实现
void pre(int n) {
for (int i = 2; i <= n; i++) {
if (!nprime[i]) {
primes.push_back(i);
}
for (int p : primes) {
if (i * p > n)break;
nprime[i * p] = 1;
if (i % p == 0)break;
}
}
}
可以发现,在筛出合数的同时,线性筛也能找到每个数的最小值因子,其中 i 的最小质因子为 p。
区间筛
筛出区间 \([L,R]\) 之间的质数,当 \(R\) 很大 \(R-L+1\) 很小时,单独使用线性筛难以应对,采用区间筛。
由于合数最小值因子 \(p\le\sqrt R\),线性筛筛出 \([1,\sqrt R]\) 之间的质数,枚举每个质数及其在区间 \([L,R]\) 的倍数即可。
积性函数
积性函数:对于 \(\gcd(x,y)=1\) 的 \(x,y\),满⾜ \(f(xy)=f(x)f(y)\) 就是积性函数。
完全积性函数:对于 \(\forall x,y\),满⾜ \(f(xy)=f(x)f(y)\) 就是积性函数。
莫比乌斯函数
根据莫比乌斯函数定义有:
设 \(p_1\) 是 \(n\) 的最小值因子,\(n^\prime=\frac n{p_1}\),有:
根据定义,采用线性筛,容易写出实现。
实现
节选自 OI Wiki
vector<int> pri;
bool not_prime[N];
int mu[N];
void pre(int n) {
mu[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!not_prime[i]) {
mu[i] = -1;
pri.push_back(i);
}
for (int pri_j : pri) {
if (i * pri_j > n) break;
not_prime[i * pri_j] = true;
if (i % pri_j == 0) {
mu[i * pri_j] = 0;
break;
}
mu[i * pri_j] = -mu[i];
}
}
}
约数个数
设 \(d_i\) 表示 \(i\) 的约数个数,\(num_i\) 表示 \(i\) 最小质因子的个数。
显然:若 \(n=\prod_{i=1}^mp_i^{c_i}\),则:
乘法原理即可证明。
过程:
设 \(x=i\times p\),
- 若 \(p\nmid i\),则 \(p\) 是 \(x\) 的新增(最小)质因子,此时 \(d_x=d_i\times(1+1)=d_i\times2\),\(num_x=1\);
- 若 \(p\mid i\),则 \(p\) 是 \(i\) 的最小质因子,此时 \(x\) 的最小值因子 \(p\) 的指数增加 \(1\),所以 \(num_x=num_i+1\),\(d_x=\frac{d_i}{num_x}\times(num_x+1)\)。
实现
void pre(int n) {
d[1] = 1;
for (int i = 2; i <= n; i++) {
if (!nprime[i]) {
primes.push_back(i);
d[i] = 2;
num[i] = 1;
}
for (int p : primes) {
if (i * p > n)break;
nprime[i * p] = 1;
if (i % p == 0) {
num[i * p] = num[i] + 1;
d[i * p] = d[i] / num[i * p] * (num[i * p] + 1);
break;
} else {
num[i * p] = 1;
d[i * p] = d[i] * 2;
}
}
}
}
约数和
设 \(f_i\) 是 \(i\) 所有约数和,\(g_i\) 表示 \(i\) 最小质因子的 \(\sum_{i=1}^kp_i\)。
首先有:若 \(n=\sum_{i=1}^mp_i^{c_i}\),则:
仍然是分类讨论,设 \(x=i\times p\):
- 若 \(p\nmid i\),则 \(p\) 是 \(x\) 的新增(最小)质因子,此时 \(f_x=f_i\times(p+1)\),\(g_x=p+1\);
- 若 \(p\mid i\),则 \(p\) 是 \(i\) 的最小质因子,此时 \(x\) 的最小值因子 \(p\) 的指数增加 \(1\),所以 \(g_x=g_i\times p+1\),\(f_x=\frac{f_x}{g_i}\times g_x\)。
实现
节选自 OI Wiki
vector<int> pri;
bool not_prime[N];
int g[N], f[N];
void pre(int n) {
g[1] = f[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!not_prime[i]) {
pri.push_back(i);
g[i] = i + 1;
f[i] = i + 1;
}
for (int pri_j : pri) {
if (i * pri_j > n) break;
not_prime[i * pri_j] = true;
if (i % pri_j == 0) {
g[i * pri_j] = g[i] * pri_j + 1;
f[i * pri_j] = f[i] / g[i] * g[i * pri_j];
break;
}
f[i * pri_j] = f[i] * f[pri_j];
g[i * pri_j] = 1 + pri_j;
}
}
}
例题
P6810 「MCOI-02」Convex Hull 凸包
设 \(d(i)=\tau(i)\),由题意得:
预处理 \(d(n)\) 即可,时间复杂度 \(\Theta(n\log n)\)。
关于时间复杂度的证明:
假设 \(n<m\),总操作次数:
实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6 + 5;
int n, m, mod, d[N], num[N];
bool nprime[N];
vector <int> primes;
void init(){
d[1] = 1;
for (int i = 2; i <= m; ++i){
if (!nprime[i]){
primes.push_back(i);
d[i] = 2;
num[i] = 1;
}
for (auto p : primes){
if (i * p > m)break;
nprime[i * p] = 1;
if (i % p == 0){
num[i * p] = num[i] + 1, d[i * p] = d[i] / num[i * p] * (num[i * p] + 1);
break;
}
num[i * p] = 1;
d[i * p] = d[i] * 2;
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> mod;
if (n > m)swap(n, m);
init();
int ans = 0;
for (int g = 1; g <= n; ++g){
int sx, sy;
sx = sy = 0;
for (int x = 1; g * x <= n; ++x){
(sx += d[g * x]) %= mod;
}
for (int y = 1; g * y <= m; ++y){
(sy += d[g * y]) %= mod;
}
(ans += sx * sy % mod) %= mod;
}
cout << ans;
return 0;
}
狄利克雷卷积
记作 \(h=f*g\)。
性质
设 \(f, g, h\) 都是数论函数. 那么, 有:
- 交换律: \(f * g = g * f\).
- 结合律: \((f * g) * h = f * (g * h)\).
- 分配律: \((f + g) * h = f * h + g * h\).
- 单位元: \(f * \varepsilon = \varepsilon * f = f\), 其中, \(\varepsilon(n) = [n = 1]\) 是卷积单位元, \([\cdot]\) 是 Iverson 括号.
- 逆元: 当且仅当 \(f(1) \neq 0\) 时, 存在 \(g\) 使得 \(f * g = g * f = \varepsilon\), 且 \(g\) 称为 \(f\) 的 Dirichlet 逆元 (Dirichlet inverse), 可以记作 \(f^{-1}\). 而且, 逆元 \(g\) 满足递推公式
\[g(n) = \frac{\varepsilon(n) - \sum_{k\ell=n, k \neq 1} f(k)g(\ell)}{f(1)}, \]
节选自 OI Wiki。
狄利克雷卷积前缀和
先用线性筛筛出 \([1,n]\) 的所有质数。
枚举 \(i\) 从 \(1\) 到 \(\frac np\),执行:
这样使 \(a_i\) 的值累加到 \(i\) 乘若干质数 \(p\) 得到的倍数上,经过所有质数的处理后,可以得到答案(分解质因数)。
时间复杂度 \(\Theta(n\log\log n)\),证明较为复杂,不再赘述。
**由此可见,狄利克雷卷积前缀和
实现
for (auto p : primes){
for (int i = 1; i * p <= n; ++i){
a[i * p] += a[i];
}
}
关于狄利克雷卷积前后缀和
我们发现,当题目中出现形如:
的式子时,可以考虑使用狄利克雷卷积前缀和优化。
类似地,若式子形如:
则可以考虑后缀和。
例题
P6810 「MCOI-02」Convex Hull 凸包
还是这道题,我们观察倒数第二步的式子:
后两个式子可以使用狄利克雷卷积后缀和优化,时间复杂度 \(\Theta(n\log\log n)\)。
只需把主函数部分代码修改为如下即可。
init();
for (int i = 1; i <= n; ++i){
a[i] = d[i];
}
for (int i = 1; i <= m; ++i){
b[i] = d[i];
}
int ans = 0;
for (auto p : primes){
for (int i = n / p; i >= 1; --i){
a[i] += a[i * p];
a[i] %= mod;
}
}
for (auto p : primes){
for (int i = m / p; i >= 1; --i){
b[i] += b[i * p];
b[i] %= mod;
}
}
for (int i = 1; i <= n; ++i){
ans += a[i] * b[i] % mod;
ans %= mod;
}
cout << ans;
Möbius 函数(莫比乌斯函数)
莫比乌斯函数
上面已经介绍由莫比乌斯函数定义得到的,更方便处理的式子,接下来给出莫比乌斯函数的严格定义:
具体地:
设 \(n=\prod_{i=1}^kp_i^{c_i}\),其中 \(p_i\) 是质数,\(c_i\in\mathbb{N^+}\),
- 若 \(n=1\),则 \(\mu(n)=1\);
- 若 \(\exists c_i>1\),则 \(\mu(n)=0\);
- 否则,\(\forall c_i=1\),\(\mu(n)=(-1)^k\),即当 \(n\) 有奇数个不同的质因子时,\(\mu(n)=-1\),偶数个即为 \(\mu(n)=1\)。
性质
对于正整数 \(n\),有:
二项式定理可以证明,具体可参考 OI Wiki。
利用狄利克雷卷积,容易发现 \(\varepsilon=\mathbf1*\mu\)。也就是说,莫比乌斯函数是常值函数 \(\mathbf1\) 的狄利克雷逆。
莫比乌斯反演
先给出式子:
若 \(f(n),g(n)\) 是两个数论式子,那么有:
直接推导较为复杂,考虑简单证法:
由狄利克雷卷积,原式等价于:
由 \(\mu\) 函数和狄利克雷卷积的性质,对左边式子进行变形:
反之易证。
这是考虑它的因数的形式,考虑倍数形式也可:
另外还有一个重要形式:
更多拓展形式可见 OI Wiki。
例题
P3911 最小公倍数之和
设 \(c(i)\) 表示 \(i\) 的出现次数,\(M\) 表示 \(\max_{i=1}^na_i\),则
关于最后一步的由来:
先给出一个等式,若 \(f(i,j)\) 是一个数论函数。
因为左右两边都是枚举 \(d\times k\le M\) 的二元组 \((d,k)\),所以是等价的。
最终的式子中,\(T\times F(T)^2\) 改写为 \(\frac{(T\times F(T))^2}{T}\),注意到分母可以转化为:
考虑使用狄利克雷卷积后缀和优化。
最终式子中,括号中的式子是可以预处理的,所以最终时间复杂度为 \(\Theta(m\log m+m\log\log m)\)。
实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e4 + 5;
int n, m, x, c[N], mu[N], a[N], g[N];
bool nprime[N];
vector <int> primes;
void init(){
mu[1] = 1;
for (int i = 2; i <= m; ++i){
if (!nprime[i]){
primes.push_back(i);
mu[i] = -1;
}
for (auto p : primes){
if (i * p > m)break;
nprime[i * p] = 1;
if (i % p == 0){
mu[i * p] = 0;
break;
}
mu[i * p] = -mu[i];
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; ++i){
cin >> x;
++c[x];
m = max(m, x);
}
init();
for (int k = 1; k <= m; ++k){
for (int T = k; T <= m; T += k){
a[T] += k * mu[k];
}
}
for (int i = 1; i <= m; ++i){
g[i] = i * c[i];
}
for (auto p : primes){
for (int i = m / p; i >= 1; --i){
g[i] += g[i * p];
}
}
int ans = 0;
for (int T = 1; T <= m; ++T){
int f = g[T] / T;
ans += T * a[T] * f * f;
}
cout << ans;
return 0;
}

浙公网安备 33010602011771号