杜教筛

杜教筛被用于处理一类数论函数的前缀和问题.对于数论函数 ff,杜教筛可以在低于线性时间的复杂度内计算 S(n)=∑i=1nf(i)S(n)=\sum_{i=1}^{n}f(i).

算法思想

我们想办法构造一个 S(n)S(n) 关于 S(⌊ni⌋)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) 的递推式.

对于任意一个数论函数 gg,必满足:

∑i=1n(f∗g)(i)=∑i=1n∑d∣ig(d)f(id)=∑i=1ng(i)S(⌊ni⌋)\begin{aligned} \sum_{i=1}^{n}(f * g)(i) & =\sum_{i=1}^{n}\sum_{d \mid i}g(d)f\left(\frac{i}{d}\right) \\ & =\sum_{i=1}^{n}g(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \end{aligned}

其中 f∗gf*g 为数论函数 ff 和 gg 的 狄利克雷卷积.

略证

g(d)f(id)g(d)f\left(\frac{i}{d}\right) 就是对所有 i≤ni\leq n 的做贡献,因此变换枚举顺序,枚举 dd,id\frac{i}{d}(分别对应新的 i,ji,j)

∑i=1n∑d∣ig(d)f(id)=∑i=1n∑j=1⌊n/i⌋g(i)f(j)=∑i=1ng(i)∑j=1⌊n/i⌋f(j)=∑i=1ng(i)S(⌊ni⌋)\begin{aligned} \sum_{i=1}^n\sum_{d \mid i}g(d)f\left(\frac{i}{d}\right) & =\sum_{i=1}^n\sum_{j=1}^{\left\lfloor n/i \right\rfloor}g(i)f(j) \\ & =\sum_{i=1}^ng(i)\sum_{j=1}^{\left\lfloor n/i \right\rfloor}f(j) \\ & =\sum_{i=1}^ng(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \end{aligned}

那么可以得到递推式:

g(1)S(n)=∑i=1ng(i)S(⌊ni⌋)−∑i=2ng(i)S(⌊ni⌋)=∑i=1n(f∗g)(i)−∑i=2ng(i)S(⌊ni⌋)\begin{aligned} g(1)S(n) & = \sum_{i=1}^n g(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) - \sum_{i=2}^n g(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \\ & = \sum_{i=1}^n (f * g)(i) - \sum_{i=2}^n g(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \end{aligned}

假如我们可以构造恰当的数论函数 gg 使得:

  1. 可以快速计算 ∑i=1n(f∗g)(i)\sum_{i=1}^n(f * g)(i);
  2. 可以快速计算 gg 的前缀和,以用数论分块求解 ∑i=2ng(i)S(⌊ni⌋)\sum_{i=2}^ng(i)S\left(\left\lfloor\dfrac{n}{i}\right\rfloor\right).

则我们可以在较短时间内求得 g(1)S(n)g(1)S(n).

注意

无论数论函数 ff 是否为积性函数,只要可以构造出恰当的数论函数 gg, 便都可以考虑用杜教筛求 ff 的前缀和.

如考虑 f(n)=iφ(n)f(n)=\mathrm{i}\varphi(n), 显然 ff 不是积性函数,但可取 g(n)=1g(n)=1, 从而:

∑k=1n(f∗g)(k)=in(n+1)2\sum_{k=1}^n (f*g)(k)=\mathrm{i}\frac{n(n+1)}{2}

计算 ∑k≤m(f∗g)(k)\sum_{k\leq m} (f*g)(k) 和 ∑k≤mg(k)\sum_{k \leq m} g(k) 的时间复杂度均为 O(1)O(1), 故可以考虑使用杜教筛.

时间复杂度

令 R(n)={⌊nk⌋:k=2,3,…,n}R(n)=\left\{\left\lfloor \dfrac{n}{k} \right\rfloor: k=2,3,\dots,n\right\}.利用数论分块的 性质 可知,对任意的 m∈R(n)m\in R(n),都有 R(m)⊆R(n)R(m)\subseteq R(n).也就是说,使用记忆化之后,只需要对所有 k∈R(n)k\in R(n) 计算一次 S(k)S(k) 就可以得到 R(n)R(n) 的值.而这些点的数目 ∣R(n)∣=O(n)|R(n)|=O(\sqrt{n}).

设计算 ∑i=1n(f∗g)(i)\sum_{i=1}^n(f * g)(i) 和 ∑i=1ng(i)\sum_{i=1}^n g(i) 的时间复杂度均为 O(1)O(1). 设计算 S(n)S(n) 的时间复杂度为 T(n)T(n), 则:

T(n)=∑k∈R(n)T(k)=Θ(n)+∑k=1⌊n⌋O(k)+∑k=2⌊n⌋O(nk)=O(∫0n(x+nx)dx)=O(n3/4).\begin{aligned} T(n) & = \sum_{k\in R(n)} T(k)\\ & = \Theta(\sqrt n)+\sum_{k=1}^{\lfloor\sqrt n\rfloor} O(\sqrt k)+\sum_{k=2}^{\lfloor\sqrt n\rfloor} O\left(\sqrt{\dfrac{n}{k}}\right)\\ & = O\left(\int_{0}^{\sqrt n} \left(\sqrt{x} + \sqrt{\dfrac{n}{x}}\right) \mathrm{d}x\right)\\ & = O\left(n^{3/4}\right). \end{aligned}

若我们可以预处理出一部分 S(k)S(k), 其中 k=1,2,…,mk=1,2,\dots,m,m≥⌊n⌋m\geq \lfloor\sqrt n\rfloor.设预处理的时间复杂度为 T0(m)T_0(m),则此时的 T(n)T(n) 为:

T(n)=T0(m)+∑k∈R(n);k>mT(k)=T0(m)+∑k=1⌊n/m⌋O(nk)=O(T0(m)+∫0n/mnxdx)=O(T0(m)+nm).\begin{aligned} T(n) & = T_0(m)+\sum_{k\in R(n);k>m} T(k)\\ & = T_0(m)+\sum_{k=1}^{\lfloor n/m \rfloor} O\left(\sqrt{\dfrac{n}{k}}\right)\\ & = O\left(T_0(m)+\int_{0}^{n/m} \sqrt{\dfrac{n}{x}} \mathrm{d}x\right)\\ & = O\left(T_0(m)+\dfrac{n}{\sqrt m}\right). \end{aligned}

若 T0(m)=O(m)T_0(m)=O(m)(如线性筛),由均值不等式可知:当 m=Θ(n2/3)m=\Theta\left(n^{2/3}\right) 时,T(n)T(n) 取得最小值 O(n2/3)O\left(n^{2/3}\right).

伪证一例

设计算 S(n)S(n) 的复杂度为 T(n)T(n), 则有:

T(n)=Θ(n)+O(∑i=2⌊n⌋T(⌊ni⌋))T(n)=\Theta\left(\sqrt{n}\right)+O\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor} T\left(\left\lfloor\frac{n}{i}\right\rfloor\right)\right) T(⌊ni⌋)=Θ(ni)+O(∑j=2⌊n/i⌋T(⌊nij⌋))=O(ni)\begin{aligned} T\left(\left\lfloor\frac{n}{i}\right\rfloor\right) & = \Theta\left(\sqrt{\frac{n}{i}}\right)+O\left(\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\frac{n}{ij}\right\rfloor\right)\right) \\ & = O\left(\sqrt{\frac{n}{i}}\right) \end{aligned}

其中,O(∑j=2⌊n/i⌋T(⌊nij⌋))O\left(\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\dfrac{n}{ij}\right\rfloor\right)\right) 视作高阶无穷小,从而可以舍去.故:

T(n)=Θ(n)+O(∑i=2⌊n⌋ni)=O(∑i=1⌊n⌋ni)=O(∫0nnxdx)=O(n3/4)\begin{aligned} T(n) & = \Theta\left(\sqrt{n}\right)+O\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor} \sqrt{\frac{n}{i}}\right) \\ & = O\left(\sum_{i=1}^{\lfloor\sqrt{n}\rfloor} \sqrt{\frac{n}{i}}\right) \\ & = O\left(\int_{0}^{\sqrt{n}}\sqrt{\frac{n}{x}}\mathrm{d}x\right) \\ & = O\left(n^{3/4}\right) \end{aligned}
Bug

问题在于「视作高阶无穷小,从而可以舍去」这一处.我们将 T(⌊ni⌋)T\left(\left\lfloor\dfrac{n}{i}\right\rfloor\right) 代入 T(n)T(n) 的式子里,有:

T(n)=Θ(n)+O(∑i=2⌊n⌋ni)+O(∑i=2⌊n⌋∑j=2⌊n/i⌋T(⌊nij⌋))=O(n+∫0nnxdx)+O(∑i=2⌊n⌋∑j=2⌊n/i⌋T(⌊nij⌋))=O(n3/4)+O(∑i=2⌊n⌋∑j=2⌊n/i⌋T(⌊nij⌋))\begin{aligned} T(n) & = \Theta\left(\sqrt{n}\right)+O\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor} \sqrt{\frac{n}{i}}\right)+O\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor}\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\frac{n}{ij}\right\rfloor\right)\right)\\ & = O\left(\sqrt{n}+\int_{0}^{\sqrt{n}}\sqrt{\frac{n}{x}}\mathrm{d}x\right)+O\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor}\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\frac{n}{ij}\right\rfloor\right)\right)\\ & = O\left(n^{3/4}\right)+O\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor}\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\frac{n}{ij}\right\rfloor\right)\right)\\ \end{aligned}

我们考虑 ∑i=2⌊n⌋∑j=2⌊n/i⌋T(⌊nij⌋)\displaystyle\sum_{i=2}^{\lfloor\sqrt{n}\rfloor}\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\frac{n}{ij}\right\rfloor\right) 这部分,不难发现:

∑i=2⌊n⌋∑j=2⌊n/i⌋T(⌊nij⌋)=Ω(∑i=2⌊n⌋T(⌊ni⋅⌊ni⌋−1⌋))=Ω(∑i=2⌊n⌋T(⌊ni⌋))\begin{aligned} \sum_{i=2}^{\lfloor\sqrt{n}\rfloor}\sum_{j=2}^{\lfloor\sqrt{n/i}\rfloor} T\left(\left\lfloor\frac{n}{ij}\right\rfloor\right) & = \Omega\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor} T\left(\left\lfloor\frac{n}{i}\cdot\left\lfloor\sqrt\frac{n}{i}\right\rfloor^{-1}\right\rfloor\right)\right) \\ & = \Omega\left(\sum_{i=2}^{\lfloor\sqrt{n}\rfloor} T\left(\left\lfloor\sqrt\frac{n}{i}\right\rfloor\right)\right) \end{aligned}

由于没有引入记忆化,因此上式中的 T(⌊ni⌋)T\left(\left\lfloor\sqrt{\dfrac{n}{i}}\right\rfloor\right) 仍然是 Ω((ni)1/4)\Omega\left(\left(\dfrac{n}{i}\right)^{1/4}\right) 的,进而所谓的「高阶无穷小」部分是不可以舍去的.

实际上杜教筛的亚线性时间复杂度是由记忆化保证的.只有使用了记忆化之后才能保证不会出现那个多重求和的项.

例题

问题一

P4213【模板】杜教筛(Sum)

求 S1(n)=∑i=1nμ(i)S_1(n)= \sum_{i=1}^{n} \mu(i) 和 S2(n)=∑i=1nφ(i)S_2(n)= \sum_{i=1}^{n} \varphi(i) 的值,1≤n<2311\leq n<2^{31}.

莫比乌斯函数前缀和

我们知道:

ϵ=[n=1]=μ∗1=∑d∣nμ(d)\epsilon = [n=1] = \mu * 1 = \sum_{d \mid n} \mu(d) S1(n)=∑i=1nϵ(i)−∑i=2nS1(⌊ni⌋)=1−∑i=2nS1(⌊ni⌋)\begin{aligned} S_1(n) & =\sum_{i=1}^n \epsilon (i)-\sum_{i=2}^n S_1 \left(\left\lfloor \frac n i \right\rfloor\right) \\ & = 1-\sum_{i=2}^n S_1\left(\left\lfloor \frac n i \right\rfloor\right) \end{aligned}

时间复杂度的推导见 时间复杂度 一节.

对于较大的值,需要用 map/unordered_map 存下其对应的值,方便以后使用时直接使用之前计算的结果.

欧拉函数前缀和

当然也可以用杜教筛求出 φ(x)\varphi (x) 的前缀和,但是更好的方法是应用莫比乌斯反演.

莫比乌斯反演

∑i=1n∑j=1n[gcd⁡(i,j)=1]=∑i=1n∑j=1n∑d∣i,d∣jμ(d)=∑d=1nμ(d)⌊nd⌋2\begin{aligned} \sum_{i=1}^n \sum_{j=1}^n [\gcd(i,j)=1] & =\sum_{i=1}^n \sum_{j=1}^n \sum_{d \mid i,d \mid j} \mu(d) \\ & =\sum_{d=1}^n \mu(d) {\left\lfloor \frac n d \right\rfloor}^2 \end{aligned}

由于题目所求的是 ∑i=1n∑j=1i[gcd⁡(i,j)=1]\sum_{i=1}^n \sum_{j=1}^i [\gcd(i,j)=1], 所以我们排除掉 i=1,j=1i=1,j=1 的情况,并将结果除以 22 即可.

观察到,只需求出莫比乌斯函数的前缀和,就可以快速计算出欧拉函数的前缀和了.时间复杂度 O(n23)O\left(n^{\frac 2 3}\right).

杜教筛

求 S(n)=∑i=1nφ(i)S(n)=\sum_{i=1}^n\varphi(i).

同样的,φ∗1=id⁡\varphi * 1=\operatorname{id}, 从而:

S(n)=∑i=1ni−∑i=2nS(⌊ni⌋)=12n(n+1)−∑i=2nS(⌊ni⌋) \begin{aligned} S(n) & =\sum_{i=1}^n i - \sum_{i=2}^n S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \\ & =\frac{1}{2}n(n+1) - \sum_{i=2}^n S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \end{aligned}
代码实现
#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」简单的数学题

大意:求

∑i=1n∑j=1ni⋅j⋅gcd⁡(i,j)(modp)\sum_{i=1}^n\sum_{j=1}^ni\cdot j\cdot\gcd(i,j)\pmod p

其中 n≤1010,5×108≤p≤1.1×109n\leq 10^{10},5\times 10^8\leq p\leq 1.1\times 10^9,pp 是质数.

利用 φ∗1=id⁡\varphi * 1=\operatorname{id} 做莫比乌斯反演化为:

∑d=1nF2(⌊nd⌋)⋅d2φ(d)\sum_{d=1}^nF^2\left(\left\lfloor\frac{n}{d}\right\rfloor\right)\cdot d^2\varphi(d)

其中 F(n)=12n(n+1)F(n)=\dfrac{1}{2}n(n+1)

对 ∑d=1nF(⌊nd⌋)2\sum_{d=1}^nF\left(\left\lfloor\dfrac{n}{d}\right\rfloor\right)^2 做数论分块,d2φ(d)d^2\varphi(d) 的前缀和用杜教筛处理:

f(n)=n2φ(n)=(id⁡2φ)(n)f(n)=n^2\varphi(n)=(\operatorname{id}^2\varphi)(n) S(n)=∑i=1nf(i)=∑i=1n(id⁡2φ)(i)S(n)=\sum_{i=1}^nf(i)=\sum_{i=1}^n(\operatorname{id}^2\varphi)(i)

需要构造积性函数 gg,使得 f×gf\times g 和 gg 能快速求和.

单纯的 φ\varphi 的前缀和可以用 φ∗1\varphi * 1 的杜教筛处理,但是这里的 ff 多了一个 id⁡2\operatorname{id}^2,那么我们就卷一个 id⁡2\operatorname{id}^2 上去,让它变成常数:

S(n)=∑i=1n((id⁡2φ)∗id⁡2)(i)−∑i=2nid⁡2(i)S(⌊ni⌋)S(n)=\sum_{i=1}^n\left(\left(\operatorname{id}^2\varphi\right) * \operatorname{id}^2\right)(i)-\sum_{i=2}^n\operatorname{id}^2(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right)

化一下卷积:

((id⁡2φ)∗id⁡2)(i)=∑d∣i(id⁡2φ)(d)id⁡2(id)=∑d∣id2φ(d)(id)2=∑d∣ii2φ(d)=i2∑d∣iφ(d)=i2(φ∗1)(i)=i3\begin{aligned} ((\operatorname{id}^2\varphi)* \operatorname{id}^2)(i) & =\sum_{d \mid i}\left(\operatorname{id}^2\varphi\right)(d)\operatorname{id}^2\left(\frac{i}{d}\right) \\ & =\sum_{d \mid i}d^2\varphi(d)\left(\frac{i}{d}\right)^2 \\ & =\sum_{d \mid i}i^2\varphi(d)=i^2\sum_{d \mid i}\varphi(d) \\ & =i^2(\varphi*1)(i)=i^3 \end{aligned}

再化一下 S(n)S(n):

S(n)=∑i=1n((id⁡2φ)∗id⁡2)(i)−∑i=2nid⁡2(i)S(⌊ni⌋)=∑i=1ni3−∑i=2ni2S(⌊ni⌋)=(12n(n+1))2−∑i=2ni2S(⌊ni⌋)\begin{aligned} S(n) & =\sum_{i=1}^n\left((\operatorname{id}^2\varphi)* \operatorname{id}^2\right)(i)-\sum_{i=2}^n\operatorname{id}^2(i)S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \\ & =\sum_{i=1}^ni^3-\sum_{i=2}^ni^2S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \\ & =\left(\frac{1}{2}n(n+1)\right)^2-\sum_{i=2}^ni^2S\left(\left\lfloor\frac{n}{i}\right\rfloor\right) \\ \end{aligned}

分块求解即可.

代码实现
// 不要为了省什么内存把数组开小,会卡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;
}

参考资料

  1. 任之洲,2016,《积性函数求和的几种方法》,2016 年信息学奥林匹克中国国家队候选队员论文
  2. 杜教筛的时空复杂度分析 - riteme.site

本文内容来自 OI Wiki《杜教筛》;作者与修订历史见原文。AIOJ 对排版、站内链接和资源路径做了格式适配。

内容依 CC BY-SA 4.0 与 SATA 许可共享;附加条款以原文声明为准。感谢 OI Wiki 作者与贡献者;请按 SATA 约定前往 OI Wiki 仓库 Star 支持。