杜教筛
杜教筛被用于处理一类数论函数的前缀和问题。对于数论函数 ,杜教筛可以在低于线性时间的复杂度内计算 。
算法思想
我们想办法构造一个 关于 的递推式。
对于任意一个数论函数 ,必满足:
其中 为数论函数 和 的 狄利克雷卷积。
略证
就是对所有 的做贡献,因此变换枚举顺序,枚举 ,(分别对应新的 )
那么可以得到递推式:
假如我们可以构造恰当的数论函数 使得:
- 可以快速计算 ;
- 可以快速计算 的前缀和,以用数论分块求解 。
则我们可以在较短时间内求得 。
注意
无论数论函数 是否为积性函数,只要可以构造出恰当的数论函数 , 便都可以考虑用杜教筛求 的前缀和。
如考虑 , 显然 不是积性函数,但可取 , 从而:
计算 和 的时间复杂度均为 , 故可以考虑使用杜教筛。
时间复杂度
令 , 我们有如下引理:
引理
对任意的 ,我们有 .
证明
令 , 任取 , 由整除分块/数论分块的 引理 1 可知:
设计算 和 的时间复杂度均为 . 由引理可知:使用记忆化之后,每个 均只会计算一次。
由整除分块/数论分块的 引理 2 可知 . 设计算 的时间复杂度为 , 则:
若我们可以预处理出一部分 , 其中 ,. 设预处理的时间复杂度为 , 则此时的 为:
若 (如线性筛),由均值不等式可知:当 时, 取得最小值 .
伪证一例
设计算 的复杂度为 , 则有:
Note
视作高阶无穷小,从而可以舍去。
故:
Bug
问题在于「视作高阶无穷小,从而可以舍去」这一处。我们将 代入 的式子里,有:
我们考虑 这部分,不难发现:
由于没有引入记忆化,因此上式中的 仍然是 的,进而所谓的「高阶无穷小」部分是不可以舍去的。
实际上杜教筛的亚线性时间复杂度是由记忆化保证的。只有使用了记忆化之后才能保证不会出现那个多重求和的项。
例题
问题一
P4213【模板】杜教筛(Sum)
求 和 的值,.
代码实现
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 | #include <cstring>
#include <iostream>
#include <map>
using namespace std;
constexpr int MAXN = 2000010;
long long T, n, pri[MAXN], cur, mu[MAXN], sum_mu[MAXN];
bool vis[MAXN];
map<long long, long long> mp_mu;
long long S_mu(long long x) { // 求mu的前缀和
if (x < MAXN) return sum_mu[x];
if (mp_mu[x]) return mp_mu[x]; // 如果map中已有该大小的mu值,则可直接返回
long long ret = (long long)1;
for (long long i = 2, j; i <= x; i = j + 1) {
j = x / (x / i);
ret -= S_mu(x / i) * (j - i + 1);
}
return mp_mu[x] = ret; // 路径压缩,方便下次计算
}
long long S_phi(long long x) { // 求phi的前缀和
long long ret = (long long)0;
long long j;
for (long long i = 1; i <= x; i = j + 1) {
j = x / (x / i);
ret += (S_mu(j) - S_mu(i - 1)) * (x / i) * (x / i);
}
return (ret - 1) / 2 + 1;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> T;
mu[1] = 1;
for (int i = 2; i < MAXN; i++) { // 线性筛预处理mu数组
if (!vis[i]) {
pri[++cur] = i;
mu[i] = -1;
}
for (int j = 1; j <= cur && i * pri[j] < MAXN; j++) {
vis[i * pri[j]] = true;
if (i % pri[j])
mu[i * pri[j]] = -mu[i];
else {
mu[i * pri[j]] = 0;
break;
}
}
}
for (int i = 1; i < MAXN; i++)
sum_mu[i] = sum_mu[i - 1] + mu[i]; // 求mu数组前缀和
while (T--) {
cin >> n;
cout << S_phi(n) << ' ' << S_mu(n) << '\n';
}
return 0;
}
|
问题二
「LuoguP3768」简单的数学题
大意:求
其中 , 是质数。
利用 做莫比乌斯反演化为:
其中
对 做数论分块, 的前缀和用杜教筛处理:
需要构造积性函数 ,使得 和 能快速求和。
单纯的 的前缀和可以用 的杜教筛处理,但是这里的 多了一个 ,那么我们就卷一个 上去,让它变成常数:
化一下卷积:
再化一下 :
分块求解即可。
代码实现
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 | // 不要为了省什么内存把数组开小,会卡80
#include <cmath>
#include <iostream>
#include <map>
using namespace std;
constexpr int N = 5e6, NP = 5e6, SZ = N;
long long n, P, inv2, inv6, s[N];
int phi[N], p[NP], cnt, pn;
bool bp[N];
map<long long, long long> s_map;
long long ksm(long long a, long long m) { // 求逆元用
long long res = 1;
while (m) {
if (m & 1) res = res * a % P;
a = a * a % P, m >>= 1;
}
return res;
}
void prime_work(int k) { // 线性筛phi,s
bp[0] = bp[1] = true, phi[1] = 1;
for (int i = 2; i <= k; i++) {
if (!bp[i]) p[++cnt] = i, phi[i] = i - 1;
for (int j = 1; j <= cnt && i * p[j] <= k; j++) {
bp[i * p[j]] = true;
if (i % p[j] == 0) {
phi[i * p[j]] = phi[i] * p[j];
break;
} else
phi[i * p[j]] = phi[i] * phi[p[j]];
}
}
for (int i = 1; i <= k; i++)
s[i] = (1ll * i * i % P * phi[i] % P + s[i - 1]) % P;
}
long long s3(long long k) { // 立方和
return k %= P, (k * (k + 1) / 2) % P * ((k * (k + 1) / 2) % P) % P;
}
long long s2(long long k) { // 平方和
return k %= P, k * (k + 1) % P * (k * 2 + 1) % P * inv6 % P;
}
long long calc(long long k) { // 计算S(k)
if (k <= pn) return s[k];
if (s_map[k]) return s_map[k]; // 对于超过pn的用map离散存储
long long res = s3(k), pre = 1, cur;
for (long long i = 2, j; i <= k; i = j + 1)
j = k / (k / i), cur = s2(j),
res = (res - calc(k / i) * (cur - pre) % P) % P, pre = cur;
return s_map[k] = (res + P) % P;
}
long long solve() {
long long res = 0, pre = 0, cur;
for (long long i = 1, j; i <= n; i = j + 1) {
j = n / (n / i);
cur = calc(j);
res = (res + (s3(n / i) * (cur - pre)) % P) % P;
pre = cur;
}
return (res + P) % P;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> P >> n;
inv2 = ksm(2, P - 2), inv6 = ksm(6, P - 2);
pn = (long long)pow(n, 0.666667); // n^(2/3)
prime_work(pn);
cout << solve();
return 0;
}
|
参考资料
- 任之洲,2016,《积性函数求和的几种方法》,2016 年信息学奥林匹克中国国家队候选队员论文
- 杜教筛的时空复杂度分析 - riteme.site
本页面最近更新:2024/3/13 16:54:18,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:Marcythm, StudyingFather, hsfzLZH1, sshwy, Backl1ght, Enter-tainer, Great-designer, Henry-ZHR, huayucaiji, Ir1d, kenlig, MegaOwIer, Menci, Nanarikom, nanmenyangde, ouuan, purple-vine, shawlleyw, Sshwy, Tiphereth-A, Xeonacid
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用