「笔记」杜教筛
前置知识
务必较为熟练地掌握上述知识。以下推导均建立在上述知识上展开。
简介
可在低于线性时间的复杂度内 处理数论函数的前缀和。
对于一个数论函数 \(f(n)\),要求快速求解:
设 \(S(n) = \sum\limits_{i=1}^{n}f(i)\),则对于任何一个数论函数 \(g\),必满足:
将 \(i=1\) 提出并移项,则可得到递推式:
发现后一项可以数论分块求解。
若可以快速求解前一项的卷积,就可以快速求得 \(g(1)S(n)\)。
证明
这是一个看起来很扯的式子:
只考虑等式右侧,展开卷积:
考虑交换枚举顺序,先枚举 \(d\)。
\(d \mid i\),则 \(i\) 为 \(d\) 的倍数(废话)。
则 \(d\) 会对其倍数 \(kd\) 作出 \(g(d)\cdot f(\dfrac{kd}{d}) = g(d)\cdot f(k)\) 的贡献。
\(1\sim n\) 中 \(d\) 的倍数为 \(d\sim \left\lfloor\dfrac{n}{d}\right\rfloor d\),则有下式:
得证。
例题
基础知识是不是非常水
板子题
\(T\) 组数据,每组数据给定 \(n\),求:
\[\begin{aligned} \sum_{i=1}^{n} \varphi(i)\\ \sum_{i=1}^{n}\mu (i) \end{aligned}\]\(1\le T\le 10,1\le n\le 2^{31}\)。
设要求解的函数为 \(f\),考虑找到一个函数 \(g\),构造一个可以快速求值的函数 \(f\ast g\)。
对于子任务 1,考虑 \(\varphi\) 的性质:
把 \(\varphi\) 和 \(1\) 卷起来,得到了易于求值的幂函数 \(\operatorname{Id}\)。
直接代入式子:
数论分块求解即可。
对于子任务 2,考虑 \(\mu\) 的性质。
把 \(\mu\) 和 \(1\) 卷起来,得到了单位元 \(e\)。
带入式子,这个更优美了:
数论分块求解即可。
代码
//知识点:杜教筛
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#include <unordered_map>
#define ll long long
const int kMaxn = 5e6 + 10;
const int kInf = 0x7fffffff;
const int kMaxPrimeNum = 348521 + 10;
//=============================================================
int limit = 5e6 + 10, pnum, prime[kMaxPrimeNum];
int mu[kMaxn];
ll phi[kMaxn];
bool vis[kMaxn];
std :: unordered_map <int, ll> kn_phi; //kn = known
std :: unordered_map <int, int> kn_mu;
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Prepare() {
mu[1] = phi[1] = 1;
for (int i = 2; i <= limit; ++ i) {
if (! vis[i]) {
prime[++ pnum] = i;
mu[i] = - 1;
phi[i] = i - 1;
}
for (int j = 1; j <= pnum; ++ j) {
if (prime[j] * i > limit) break;
int pj = prime[j];
vis[pj * i] = true;
if (! (i % pj)) {
phi[i * pj] = phi[i] * pj;
break;
}
phi[i * pj] = phi[i] * phi[pj];
mu[i * pj] = - mu[i];
}
}
for (int i = 1; i <= limit; ++ i) {
mu[i] += mu[i - 1];
phi[i] += phi[i - 1];
}
}
ll SumPhi(int n) {
if (n <= limit) return phi[n];
if (kn_phi.count(n)) return kn_phi[n];
ll ret = 1ll * n * (1ll * n + 1) >> 1;
for (int l = 2, r; r < kInf && l < n; l = r + 1) {
r = n / (n / l);
ret -= 1ll * (r - l + 1) * SumPhi(n / l);
}
return (kn_phi[n] = ret);
}
ll SumMu(int n) {
if (n <= limit) return mu[n];
if (kn_mu.count(n)) return kn_mu[n];
int ret = 1;
for (int l = 2, r; r < kInf && l < n; l = r + 1) {
r = n / (n / l);
ret -= (r - l + 1) * SumMu(n / l);
}
return (kn_mu[n] = ret);
}
//=============================================================
int main() {
Prepare();
int T = read();
while (T --) {
int n = read();
printf("%lld %d\n", SumPhi(n), SumMu(n));
}
return 0;
}
感悟
杜教筛的过程,相当于找到一个函数 \(g\),
构造一个可以快速求值的函数 \(h = f\ast g\)。
那要怎么找?大胆猜测
一般是常见的函数,建议多做题多感悟。
写在最后
参考资料:
在别人作画的时候随便加上两笔?
这太野蛮了,太谔谔了,
我不承认这是人做出来的事。