P4213 学习笔记 & 杜教筛详解
杜教筛
我们之前通过线性筛(欧拉筛)求出了 \(\mu(i)\) 和 \(\phi(i)\) 的前缀和,以优秀的 \(O(n)\) 抢占先机。
但是,我们在 \(O(nq)\) 的时间复杂度下百万量级的数据是过不了的。
你不要跟我说 WC2026 的文艺汇演说
我们的每一档部分分都是意料之外的
暴力的,\(O(nq)\) 是可过的
那个纯属节目效果。
杜教筛可以优化为 \(O(n^{\frac 23})\),你可能觉得差别微小,但是人家 \(n \le 2^{31}-1\) 差别就大了。
设我们现在需要求前缀和的积性函数为 \(f(x)\),则我们现在是要以优秀的时间复杂度算出
\[S(n)=\sum_{i=1}^n f(i)
\]
我们考虑再构造两个积性函数 \(h\) 和 \(g\),满足卷积
\[h=g \cdot f
\]
则
\[h(i)=\sum_{d \mid i} g(d)f(\frac id)
\]
对其求和,有
\[\begin{align*}
\sum_{i=1}^n h(i)&=\sum_{i=1}^n \sum_{d \mid i} g(d) f(\frac id)\\
&=\sum_{d=1}^n g(d) \sum_{d \mid i} f(\frac id)\\
&=\sum_{d=1}^n g(d) \sum_{i=1}^{\lfloor \frac nd \rfloor} f(i)\\
&=\sum_{d=1}^n g(d) S(\lfloor \frac nd \rfloor)\\
\end{align*}
\]
(称上述推导中第一行到第二行为杜教筛变换)
而我们需要计算 \(S(n)\),于是考虑把 \(d=1\) 的项提出来,变成
\[\sum_{i=1}^n h(i)=g(1)S(n)+\sum_{d=2}^n g(d) S(\lfloor \frac nd \rfloor)
\]
于是
\[g(1)S(n)=\sum_{i=1}^n h(i)-\sum_{d=2}^n g(d) S(\lfloor \frac nd \rfloor)
\]
即杜教筛公式。
公式中形似整除分块的东西用整除分块算就行了。
P4213
我们需要求积性函数 \(\phi(i)\) 和 \(\mu(i)\) 的前缀和。
\(\phi(i)\)
先套公式,我们需要定义卷积
\[h=g \cdot f=\sum_{d \mid n} f(d) g(\frac nd)
\]
把
\[n=\sum_{d \mid n} \phi(d)
\]
改写为
\[id=\phi \cdot I
\]
从而
\[h=id,g=I
\]
代入公式
\[g(1)S(n)=\sum_{i=1}^n h(i)-\sum_{i=2}^n g(i) S(\lfloor \frac ni \rfloor)
\]
得
\[S(n)=\sum_{i=1}^n i-\sum_{i=2}^n S(\lfloor \frac ni \rfloor)=\frac 12 n(n+1)-\sum_{i=2}^n S(\lfloor \frac ni \rfloor)
\]
整除分块。
\(\mu(i)\)
首先,\(\mu(i)\) 有性质
\[\sum_{d \mid n} \mu(d)=\begin{cases}
1 & n=1\\
0 & n>1
\end{cases}
\]
为了方便,我们记为
\[\sum_{d \mid n} \mu(d)=[n=1]
\]
套用公式,构造卷积
\[h=f \cdot g=\sum_{d \mid n} f(d) g(\frac nd)
\]
把
\[\sum_{d \mid n} \mu(d)=[n=1]
\]
改写为
\[\varepsilon =\mu \cdot I
\]
则
\[h=\varepsilon,g=I
\]
代入
\[g(1)S(n)=\sum_{i=1}^n h(i)-\sum_{i=2}^n g(d)S(\lfloor \frac ni \rfloor)
\]
得
\[S(n)=\sum_{i=1}^n \varepsilon(i)-\sum_{i=2}^n S(\lfloor \frac ni \rfloor)=1-\sum_{i=2}^n S(\lfloor \frac ni \rfloor)
\]
code
#include <bits/stdc++.h>
#define pub public:
#define pri private:
#define fri friend:
#define Ofile(s) freopen(s".in", "r", stdin), freopen (s".out", "w", stdout)
#define Cfile(s) fclose(stdin), fclose(stdout)
#define fast ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
using namespace std;
using ll = long long;
using ull = unsigned long long;
using lb = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using pil = pair<int, ll>;
using pli = pair<ll, int>;
constexpr int mod = 998244353;
constexpr int maxn = 5e6 + 5;
ll t;
ll n, tot;
ll prime[maxn], mu[maxn], phi[maxn];
bool vis[maxn];
unordered_map <ll, ll> sum_mu, sum_phi; // 用来记忆化的两个东西
void Du_sieve(); // 杜教筛
ll getsmu(ll x); // mu(i) 的前缀和
ll getsphi(ll x); // phi(i) 的前缀和
int main() {
freopen("std.in", "r", stdin);
freopen("std.out", "w", stdout);
fast;
Du_sieve();
cin >> t;
while (t--){
cin >> n;
cout << getsphi(n) << ' ' << getsmu(n) << endl;
}
return 0;
}
void Du_sieve(){
vis[0] = vis[1] = 1;
mu[1] = phi[1] = 1;
for (ll i = 2; i <= maxn - 5; i++){
if (!vis[i]){
prime[++tot] = i;
mu[i] = -1;
phi[i] = i - 1;
}
for (ll j = 1; j <= tot && i * prime[j] <= maxn - 5; j++){
vis[i * prime[j]] = true;
if (i % prime[j]){
mu[i * prime[j]] = -mu[i];
phi[i * prime[j]] = phi[i] * phi[prime[j]];
}
else {
mu[i * prime[j]] = 0;
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
}
} // 线性筛先预处理一部分
for (ll i = 1; i <= maxn - 5; i++)
mu[i] += mu[i - 1], phi[i] += phi[i - 1]; // 把两个数组变成存储前缀和
}
ll getsmu(ll x){
if (x <= maxn - 5)
return mu[x]; // 有直接用的就直接用
if (sum_mu[x])
return sum_mu[x]; // 记忆化,只算一次
ll res = 1; // 刚刚推导出的1
for (ll pl = 2, pr; pl <= x; pl = pr + 1){ // 整除分块
pr = x / (x / pl);
res -= (pr - pl + 1) * getsmu(x / pl);
}
return sum_mu[x] = res; // 记得修改
}
ll getsphi(ll x){
if (x <= maxn - 5)
return phi[x]; // 有直接用的就直接用
if (sum_phi[x])
return sum_phi[x]; // 记忆化,只算一次
ll res = x * (x + 1) / 2; // 公式中 1~x 的和
for (ll pl = 2, pr; pl <= x; pl = pr + 1){ // 整除分块
pr = x / (x / pl);
res -= (pr - pl + 1) * getsphi(x / pl);
}
return sum_phi[x] = res; // 记得修改
}

浙公网安备 33010602011771号