杜教筛
杜教筛被用于处理一类数论函数的前缀和问题。对于数论函数 𝑓
,杜教筛可以在低于线性时间的复杂度内计算 𝑆(𝑛) =∑𝑛𝑖=1𝑓(𝑖)
。
算法思想
我们想办法构造一个 𝑆(𝑛)
 关于 𝑆(⌊𝑛𝑖⌋)
 的递推式。
对于任意一个数论函数 𝑔
,必满足:
𝑛∑𝑖=1(𝑓∗𝑔)(𝑖)=𝑛∑𝑖=1∑𝑑∣𝑖𝑔(𝑑)𝑓(𝑖𝑑)=𝑛∑𝑖=1𝑔(𝑖)𝑆(⌊𝑛𝑖⌋)
其中 𝑓 ∗𝑔
 为数论函数 𝑓
 和 𝑔
 的 狄利克雷卷积。
略证
 𝑔(𝑑)𝑓(𝑖𝑑)
 就是对所有 𝑖 ≤𝑛
 的做贡献,因此变换枚举顺序,枚举 𝑑
,𝑖𝑑
(分别对应新的 𝑖,𝑗
)
 𝑛∑𝑖=1∑𝑑∣𝑖𝑔(𝑑)𝑓(𝑖𝑑)=𝑛∑𝑖=1⌊𝑛/𝑖⌋∑𝑗=1𝑔(𝑖)𝑓(𝑗)=𝑛∑𝑖=1𝑔(𝑖)⌊𝑛/𝑖⌋∑𝑗=1𝑓(𝑗)=𝑛∑𝑖=1𝑔(𝑖)𝑆(⌊𝑛𝑖⌋)
那么可以得到递推式:
𝑔(1)𝑆(𝑛)=𝑛∑𝑖=1𝑔(𝑖)𝑆(⌊𝑛𝑖⌋)−𝑛∑𝑖=2𝑔(𝑖)𝑆(⌊𝑛𝑖⌋)=𝑛∑𝑖=1(𝑓∗𝑔)(𝑖)−𝑛∑𝑖=2𝑔(𝑖)𝑆(⌊𝑛𝑖⌋)
假如我们可以构造恰当的数论函数 𝑔
 使得:
- 可以快速计算 ∑𝑛𝑖=1(𝑓 ∗𝑔)(𝑖)
; - 可以快速计算 𝑔
 的前缀和,以用数论分块求解 ∑𝑛𝑖=2𝑔(𝑖)𝑆(⌊𝑛𝑖⌋)
。 
则我们可以在较短时间内求得 𝑔(1)𝑆(𝑛)
。
注意
 无论数论函数 𝑓
 是否为积性函数,只要可以构造出恰当的数论函数 𝑔
, 便都可以考虑用杜教筛求 𝑓
 的前缀和。
 如考虑 𝑓(𝑛) =i𝜑(𝑛)
, 显然 𝑓
 不是积性函数,但可取 𝑔(𝑛) =1
, 从而:
 𝑛∑𝑘=1(𝑓∗𝑔)(𝑘)=i𝑛(𝑛+1)2
 计算 ∑𝑘≤𝑚(𝑓 ∗𝑔)(𝑘)
 和 ∑𝑘≤𝑚𝑔(𝑘)
 的时间复杂度均为 𝑂(1)
, 故可以考虑使用杜教筛。
时间复杂度
令 𝑅(𝑛) ={⌊𝑛𝑘⌋:𝑘=2,3,…,𝑛}
。利用数论分块的 性质 可知,对任意的 𝑚 ∈𝑅(𝑛)
,都有 𝑅(𝑚) ⊆𝑅(𝑛)
。也就是说,使用记忆化之后,只需要对所有 𝑘 ∈𝑅(𝑛)
 计算一次 𝑆(𝑘)
 就可以得到 𝑅(𝑛)
 的值。而这些点的数目 |𝑅(𝑛)| =𝑂(√𝑛)
。
设计算 ∑𝑛𝑖=1(𝑓 ∗𝑔)(𝑖)
 和 ∑𝑛𝑖=1𝑔(𝑖)
 的时间复杂度均为 𝑂(1)
. 设计算 𝑆(𝑛)
 的时间复杂度为 𝑇(𝑛)
, 则:
𝑇(𝑛)=∑𝑘∈𝑅(𝑛)𝑇(𝑘)=Θ(√𝑛)+⌊√𝑛⌋∑𝑘=1𝑂(√𝑘)+⌊√𝑛⌋∑𝑘=2𝑂(√𝑛𝑘)=𝑂(∫√𝑛0(√𝑥+√𝑛𝑥)d𝑥)=𝑂(𝑛3/4).
若我们可以预处理出一部分 𝑆(𝑘)
, 其中 𝑘 =1,2,…,𝑚
,𝑚 ≥⌊√𝑛⌋
。设预处理的时间复杂度为 𝑇0(𝑚)
,则此时的 𝑇(𝑛)
 为:
𝑇(𝑛)=𝑇0(𝑚)+∑𝑘∈𝑅(𝑛);𝑘>𝑚𝑇(𝑘)=𝑇0(𝑚)+⌊𝑛/𝑚⌋∑𝑘=1𝑂(√𝑛𝑘)=𝑂(𝑇0(𝑚)+∫𝑛/𝑚0√𝑛𝑥d𝑥)=𝑂(𝑇0(𝑚)+𝑛√𝑚).
若 𝑇0(𝑚) =𝑂(𝑚)
(如线性筛),由均值不等式可知:当 𝑚 =Θ(𝑛2/3)
 时,𝑇(𝑛)
 取得最小值 𝑂(𝑛2/3)
.
伪证一例
 设计算 𝑆(𝑛)
 的复杂度为 𝑇(𝑛)
, 则有:
 𝑇(𝑛)=Θ(√𝑛)+𝑂⎛⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2𝑇(⌊𝑛𝑖⌋)⎞⎟ ⎟⎠
 𝑇(⌊𝑛𝑖⌋)=Θ(√𝑛𝑖)+𝑂⎛⎜ ⎜ ⎜⎝⌊√𝑛/𝑖⌋∑𝑗=2𝑇(⌊𝑛𝑖𝑗⌋)⎞⎟ ⎟ ⎟⎠=𝑂(√𝑛𝑖)
 其中,𝑂(∑⌊√𝑛/𝑖⌋𝑗=2𝑇(⌊𝑛𝑖𝑗⌋))
 视作高阶无穷小,从而可以舍去。故:
 𝑇(𝑛)=Θ(√𝑛)+𝑂⎛⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2√𝑛𝑖⎞⎟ ⎟⎠=𝑂⎛⎜ ⎜⎝⌊√𝑛⌋∑𝑖=1√𝑛𝑖⎞⎟ ⎟⎠=𝑂(∫√𝑛0√𝑛𝑥d𝑥)=𝑂(𝑛3/4)
 Bug
 问题在于「视作高阶无穷小,从而可以舍去」这一处。我们将 𝑇(⌊𝑛𝑖⌋)
 代入 𝑇(𝑛)
 的式子里,有:
 𝑇(𝑛)=Θ(√𝑛)+𝑂⎛⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2√𝑛𝑖⎞⎟ ⎟⎠+𝑂⎛⎜ ⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2⌊√𝑛/𝑖⌋∑𝑗=2𝑇(⌊𝑛𝑖𝑗⌋)⎞⎟ ⎟ ⎟⎠=𝑂(√𝑛+∫√𝑛0√𝑛𝑥d𝑥)+𝑂⎛⎜ ⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2⌊√𝑛/𝑖⌋∑𝑗=2𝑇(⌊𝑛𝑖𝑗⌋)⎞⎟ ⎟ ⎟⎠=𝑂(𝑛3/4)+𝑂⎛⎜ ⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2⌊√𝑛/𝑖⌋∑𝑗=2𝑇(⌊𝑛𝑖𝑗⌋)⎞⎟ ⎟ ⎟⎠
 我们考虑 ⌊√𝑛⌋∑𝑖=2⌊√𝑛/𝑖⌋∑𝑗=2𝑇(⌊𝑛𝑖𝑗⌋)
 这部分,不难发现:
 ⌊√𝑛⌋∑𝑖=2⌊√𝑛/𝑖⌋∑𝑗=2𝑇(⌊𝑛𝑖𝑗⌋)=Ω⎛⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2𝑇⎛⎜ ⎜⎝⎢ ⎢ ⎢⎣𝑛𝑖⋅⌊√𝑛𝑖⌋−1⎥ ⎥ ⎥⎦⎞⎟ ⎟⎠⎞⎟ ⎟⎠=Ω⎛⎜ ⎜⎝⌊√𝑛⌋∑𝑖=2𝑇(⌊√𝑛𝑖⌋)⎞⎟ ⎟⎠
 由于没有引入记忆化,因此上式中的 𝑇(⌊√𝑛𝑖⌋)
 仍然是 Ω((𝑛𝑖)1/4)
 的,进而所谓的「高阶无穷小」部分是不可以舍去的。
 实际上杜教筛的亚线性时间复杂度是由记忆化保证的。只有使用了记忆化之后才能保证不会出现那个多重求和的项。
例题
问题一
P4213【模板】杜教筛(Sum)
 求 𝑆1(𝑛) =∑𝑛𝑖=1𝜇(𝑖)
 和 𝑆2(𝑛) =∑𝑛𝑖=1𝜑(𝑖)
 的值,1 ≤𝑛 <231
.
代码实现
  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𝑛∑𝑗=1𝑖⋅𝑗⋅gcd(𝑖,𝑗)(mod𝑝)
 其中 𝑛 ≤1010,5 ×108 ≤𝑝 ≤1.1 ×109
,𝑝
 是质数。
利用 𝜑 ∗1 =id
 做莫比乌斯反演化为:
𝑛∑𝑑=1𝐹2(⌊𝑛𝑑⌋)⋅𝑑2𝜑(𝑑)
其中 𝐹(𝑛) =12𝑛(𝑛 +1)
对 ∑𝑛𝑑=1𝐹(⌊𝑛𝑑⌋)2
 做数论分块,𝑑2𝜑(𝑑)
 的前缀和用杜教筛处理:
𝑓(𝑛)=𝑛2𝜑(𝑛)=(id2𝜑)(𝑛)
𝑆(𝑛)=𝑛∑𝑖=1𝑓(𝑖)=𝑛∑𝑖=1(id2𝜑)(𝑖)
需要构造积性函数 𝑔
,使得 𝑓 ×𝑔
 和 𝑔
 能快速求和。
单纯的 𝜑
 的前缀和可以用 𝜑 ∗1
 的杜教筛处理,但是这里的 𝑓
 多了一个 id2
,那么我们就卷一个 id2
 上去,让它变成常数:
𝑆(𝑛)=𝑛∑𝑖=1((id2𝜑)∗id2)(𝑖)−𝑛∑𝑖=2id2(𝑖)𝑆(⌊𝑛𝑖⌋)
化一下卷积:
((id2𝜑)∗id2)(𝑖)=∑𝑑∣𝑖(id2𝜑)(𝑑)id2(𝑖𝑑)=∑𝑑∣𝑖𝑑2𝜑(𝑑)(𝑖𝑑)2=∑𝑑∣𝑖𝑖2𝜑(𝑑)=𝑖2∑𝑑∣𝑖𝜑(𝑑)=𝑖2(𝜑∗1)(𝑖)=𝑖3
再化一下 𝑆(𝑛)
:
𝑆(𝑛)=𝑛∑𝑖=1((id2𝜑)∗id2)(𝑖)−𝑛∑𝑖=2id2(𝑖)𝑆(⌊𝑛𝑖⌋)=𝑛∑𝑖=1𝑖3−𝑛∑𝑖=2𝑖2𝑆(⌊𝑛𝑖⌋)=(12𝑛(𝑛+1))2−𝑛∑𝑖=2𝑖2𝑆(⌊𝑛𝑖⌋)
分块求解即可。
代码实现
  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
 
本页面最近更新:2025/10/20 12:59:22,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:StudyingFather, Tiphereth-A, hsfzLZH1, Ir1d, Enter-tainer, sshwy, c-forrest, Marcythm, MegaOwIer, Henry-ZHR, Xeonacid, Backl1ght, Great-designer, huayucaiji, kenlig, ksyx, Menci, Nanarikom, nanmenyangde, ouuan, purple-vine, shawlleyw, Sshwy
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用