杜教筛
创建时间:2026-01-06
杜教筛的概念及思想
在大部分问题中线性筛的时间复杂度已经足够优秀了,但当数据范围达到 \(10^9\)、\(10^{10}\) 甚至更高时,线性筛就太慢了,需要更快的算法解决问题。而这时就可以考虑我们强大的杜教主杜老师引进到国内的,一种快速解决数论函数的算法——杜教筛!
相较于线性筛的 \(O(n)\),杜教筛可以在 \(O(n^{\frac 2 3})\) 的时间内求解,在数论中算很大的优化了,不过杜教筛的应用场景会狭窄一些。
既然线性筛的 \(O(n)\) 已经是基于枚举的筛法的极限了,那想要做到更好就一定要用其他的方式计算。而在数论中比枚举还快的算法,最好用的便是数论分块。
杜教筛的核心思路便是将原函数转变为可以通过数论分块计算的形式,从而优化时间复杂度。
杜教筛的原理
具体的,对于数论函数 \(f(n)\),设其求和函数 \(S(n)=\sum_{i=1}^n f(i)\),现在要快速求出 \(S(n)\)。
不妨设有数论函数 \(g(n)\),\(f\) 和 \(g\) 的迪利克雷卷积 \(f * g = h\),即 \(h(n) = \sum_{d \mid n} f(d) g(\frac n d)\)。考虑令 \(H(n) = \sum_{i=1}^n h(i)\),则
将 \(S(n)\) 视为未知数解方程,解得:
这是一个递归函数的形式,当 \(g\) 和 \(H\) 都容易计算时,\(S\) 也可以通过记忆化搜索快速得到。
杜教筛的具体应用
以P4213 【模板】杜教筛为例,题目要求下面两个式子的值:
以一式为例,求 \(\sum_{i=1}^n \varphi(i)\),即 \(f(n)=\varphi(n)\),求 \(S(n)\)。
注意到 \(\varphi * \textbf 1 = Id\),即 \({\varphi * \textbf 1}(n) = n\),所以不妨令 \(g = \textbf 1\),则
故
所以
这个式子我们就很熟悉了,可以用数论分块以 \(O(\sqrt n)\) 的时间复杂度计算出来。为算法总保证时间复杂度,采用当 \(n \le N^{\frac 2 3}\) 时,通过线性筛预处理计算;当 \(n > N^{\frac 2 3}\) 时,带入式子递归计算。时间复杂度为 \(O(n^{\frac 2 3})\),证明比较复杂,详见OI wiki。
同样的,对于莫比乌斯函数,令 \(g = \textbf 1\),则 \(h = \varepsilon\),所以,\(H = \sum_{i=1} \varepsilon(i) = \textbf 1\),类比欧拉函数带入上面的递归式即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1.7e6;
int T, n, phi[N], mu[N];
bool vis[N];
vector<int> primes;
int sphi[N], smu[N];
unordered_map<int, int> dphi, dmu;
void init() {
phi[1] = mu[1] = 1;
for (int i = 2; i < N; i++) {
if (!vis[i]) {
primes.push_back(i);
phi[i] = i - 1;
mu[i] = -1;
}
for (int j : primes) {
if (i * j >= N)
break;
vis[i * j] = true;
if (i % j == 0) {
phi[i * j] = phi[i] * j;
break;
}
phi[i * j] = phi[i] * (j - 1);
mu[i * j] = -mu[i];
}
}
for (int i = 1; i < N; i++) {
sphi[i] = sphi[i - 1] + phi[i];
smu[i] = smu[i - 1] + mu[i];
}
}
int sum_phi(int n) {
if (n < N)
return sphi[n];
if (dphi.count(n))
return dphi[n];
int res = (n + 1) * n / 2;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
res -= (r - l + 1) * sum_phi(n / l);
}
return dphi[n] = res;
}
int sum_mu(int n) {
if (n < N)
return smu[n];
if (dmu.count(n))
return dmu[n];
int res = 1;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
res -= (r - l + 1) * sum_mu(n / l);
}
return dmu[n] = res;
}
signed main() {
init();
cin >> T;
while (T--) {
cin >> n;
cout << sum_phi(n) << ' ' << sum_mu(n) << '\n';
}
return 0;
}
杜教筛的其它应用
由于杜教筛仅能筛数论函数的前缀和,所以杜教筛一般和数论分块、莫比乌斯反演、欧拉反演等结合使用。
Problems:
P3768 简单的数学题(欧拉反演+数论分块+杜教筛);
P6055 [RC-02] GCD(莫比乌斯反演+数论分块+杜教筛);
P10186 [YDOI R1] Lattice(欧拉反演+数论分块+杜教筛);
P9238 [蓝桥杯 2023 省 A] 翻转硬币;

浙公网安备 33010602011771号