Processing math: 100%
杜教筛学习笔记

杜教筛能够在低于线性复杂度的时间内求出积性函数前缀和。


有两个数论函数 f,g,令

h(x)=d|xf(d)g(xd)

则称 hfg 的 Dirichlet 卷积,即 h=fg

两个数论函数的 Dirichlet 卷积也是数论函数。


对于一个数论函数,要求出 S(n)=ni=1f(i)

构造 g,h 使得 h=fg,则

ni=1h(i)=ni=1d|1g(d)f(id)=nd=1g(d)nd|if(id)=nd=1g(d)ndi=1f(i)=nd=1g(d)S(nd)=g(1)S(n)+nd=2g(d)S(nd)S(n)=ni=1h(i)nd=2g(d)S(nd)g(1)

上面的式子实际上是给出了一个关于 S 的递推式。直接这样搞是 O(n34) 的。可以线性筛出前 n23 个答案,这样总复杂度是 O(n23),都不会证


Id=φ1S(n)=n(n+1)2nd=2S(nd)ϵ=μ1S(n)=1nd=2S(nd)Id2=(φ×Id)Idφ=μId

目前网络上的杜教筛代码大都存在两个问题:

  1. std::unordered_map 的代码在判断是否存在时直接 touch 一下来判等而不是 ::count违背了 STL 的美学(当然,由于这个值最后总是要插入的,加了个小常数不算啥事)

  2. 判断 Sμ 是否计算过时直接和 0 判等。Sμ 的取值在某些情况下本来就是 0,每次都要重新计算一遍相当于凭空产生常数。(对理论复杂度有无影响不会算)

两个问题很好解决,对于用 std::unordered_map 的时候这两个问题可以用 ::count 一起改掉;如果用了数组优化,第二个问题需要用其他手段记录一下。


给一下洛谷模板题的代码。

如果 时空要求都不严格 或是像这题一样多组询问 可以用 std::unordered_map 进行记忆化。

下面的代码可以 320ms(评测鸭)通过极限数据(10 组询问,2147483638 ~ 2147483647):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<bitset>
#include<iostream>
#include<unordered_map>

using ll = long long;

constexpr unsigned base = 1664510;

std::bitset<1664513> v;
int p[125641], mu[1664513];
phi[1664513];
std::unordered_map<unsigned, ll> mu_, phi_;

void sieve(unsigned N); // 普通地线筛出 1~N 的 mu 和 phi 值

ll getMu(unsigned N) {
if (N <= base) return mu[N];
if (mu_.count(N)) return mu_[N];
ll ans = 0;
for (unsigned l = 2, r, L; l <= N; l = r + 1) {
L = N / l, r = N / L;
ans += (r - l + 1) * getMu(L);
}
return mu_[N] = 1ll - ans;
}

ll getPhi(unsigned N) {
if (N <= base) return phi[N];
if (phi_.count(N)) return phi_[N];
ll ans = 0;
for (unsigned l = 2, r, L; l <= N; l = r + 1) {
L = N / l, r = N / L;
ans += (r - l + 1) * getPhi(L);
}
return phi_[N] = (1ull * N * (N + 1ull) >> 1) - ans;
}

int main() {
unsigned T, N;

sieve(base);
for (std::cin >> T; T; --T) {
std::cin >> N;
std::cout << getPhi(N) << ' ' << getMu(N) << '\n';
}
return 0;
}


但是,std::unordered_map 的大常数有时令人难以接受。发现对于每次询问,需要的 S 值都是形如 S(nd) 的,因此不会超过 n 个。

可以再开一个大小为 n 的数组进行记忆化。(我的实现中用的是 n23

注意对于每个 nf(nd) 的值是不一样的,因此每次询问需要清空重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 单次询问更优,但多次询问在极限数据下只能通过 T = 5
// 不过谷的板题数据很水所以可以过(

#include<bitset>
#include<iostream>

using ll = long long;

constexpr unsigned base = 1664510;

std::bitset<1664513> v, vm, vp;
int MX, p[125641], mu[1664510], mu_[1664510];
ll phi[1664510], phi_[1664510];

inline int to(int x) {
return MX / x;
}

void sieve(unsigned N); // 同上

int getMu(unsigned N) {
if (N <= base) return mu[N];
if (vm[to(N)]) return mu_[to(N)];
int ans = 0;
for (unsigned l = 2, r, L; l <= N; l = r + 1) {
L = N / l, r = N / L;
ans += (r - l + 1) * getMu(L);
}
vm.set(to(N));
return mu_[to(N)] = 1 - ans;
}

ll getPhi(unsigned N) {
if (N <= base) return phi[N];
if (vp[to(N)]) return phi_[to(N)];
ll ans = 0;
for (unsigned l = 2, r, L; l <= N; l = r + 1) {
L = N / l, r = N / L;
ans += (r - l + 1) * getPhi(L);
}
vp.set(to(N));
return phi_[to(N)] = (1ull * N * (N + 1ull) >> 1) - ans;
}

int main() {
unsigned T, N;

sieve(base);
for (std::cin >> T; T; --T) {
vm.reset(), vp.reset();

std::cin >> N;
MX = N;
std::cout << getPhi(N) << ' ' << getMu(N) << '\n';
}
return 0;
}

文章作者: fa_555
文章链接: https://blog.fa555.tech/2020/%E6%9D%9C%E6%95%99%E7%AD%9B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 fa_555's Blog