杜教筛被用于处理一类数论函数的前缀和问题.对于数论函数 f,杜教筛可以在低于线性时间的复杂度内计算 S(n)=∑i=1nf(i).
算法思想
我们想办法构造一个 S(n) 关于 S(⌊in⌋) 的递推式.
对于任意一个数论函数 g,必满足:
i=1∑n(f∗g)(i)=i=1∑nd∣i∑g(d)f(di)=i=1∑ng(i)S(⌊in⌋)
其中 f∗g 为数论函数 f 和 g 的 狄利克雷卷积.
略证
g(d)f(di) 就是对所有 i≤n 的做贡献,因此变换枚举顺序,枚举 d,di(分别对应新的 i,j)
i=1∑nd∣i∑g(d)f(di)=i=1∑nj=1∑⌊n/i⌋g(i)f(j)=i=1∑ng(i)j=1∑⌊n/i⌋f(j)=i=1∑ng(i)S(⌊in⌋)
那么可以得到递推式:
g(1)S(n)=i=1∑ng(i)S(⌊in⌋)−i=2∑ng(i)S(⌊in⌋)=i=1∑n(f∗g)(i)−i=2∑ng(i)S(⌊in⌋)
假如我们可以构造恰当的数论函数 g 使得:
- 可以快速计算 ∑i=1n(f∗g)(i);
- 可以快速计算 g 的前缀和,以用数论分块求解 ∑i=2ng(i)S(⌊in⌋).
则我们可以在较短时间内求得 g(1)S(n).
注意
无论数论函数 f 是否为积性函数,只要可以构造出恰当的数论函数 g, 便都可以考虑用杜教筛求 f 的前缀和.
如考虑 f(n)=iφ(n), 显然 f 不是积性函数,但可取 g(n)=1, 从而:
k=1∑n(f∗g)(k)=i2n(n+1)
计算 ∑k≤m(f∗g)(k) 和 ∑k≤mg(k) 的时间复杂度均为 O(1), 故可以考虑使用杜教筛.
时间复杂度
令 R(n)={⌊kn⌋:k=2,3,…,n}.利用数论分块的 性质 可知,对任意的 m∈R(n),都有 R(m)⊆R(n).也就是说,使用记忆化之后,只需要对所有 k∈R(n) 计算一次 S(k) 就可以得到 R(n) 的值.而这些点的数目 ∣R(n)∣=O(n).
设计算 ∑i=1n(f∗g)(i) 和 ∑i=1ng(i) 的时间复杂度均为 O(1). 设计算 S(n) 的时间复杂度为 T(n), 则:
T(n)=k∈R(n)∑T(k)=Θ(n)+k=1∑⌊n⌋O(k)+k=2∑⌊n⌋O(kn)=O(∫0n(x+xn)dx)=O(n3/4).
若我们可以预处理出一部分 S(k), 其中 k=1,2,…,m,m≥⌊n⌋.设预处理的时间复杂度为 T0(m),则此时的 T(n) 为:
T(n)=T0(m)+k∈R(n);k>m∑T(k)=T0(m)+k=1∑⌊n/m⌋O(kn)=O(T0(m)+∫0n/mxndx)=O(T0(m)+mn).
若 T0(m)=O(m)(如线性筛),由均值不等式可知:当 m=Θ(n2/3) 时,T(n) 取得最小值 O(n2/3).
伪证一例
设计算 S(n) 的复杂度为 T(n), 则有:
T(n)=Θ(n)+Oi=2∑⌊n⌋T(⌊in⌋)
T(⌊in⌋)=Θ(in)+Oj=2∑⌊n/i⌋T(⌊ijn⌋)=O(in)
其中,O(∑j=2⌊n/i⌋T(⌊ijn⌋)) 视作高阶无穷小,从而可以舍去.故:
T(n)=Θ(n)+Oi=2∑⌊n⌋in=Oi=1∑⌊n⌋in=O(∫0nxndx)=O(n3/4)
Bug
问题在于「视作高阶无穷小,从而可以舍去」这一处.我们将 T(⌊in⌋) 代入 T(n) 的式子里,有:
T(n)=Θ(n)+Oi=2∑⌊n⌋in+Oi=2∑⌊n⌋j=2∑⌊n/i⌋T(⌊ijn⌋)=O(n+∫0nxndx)+Oi=2∑⌊n⌋j=2∑⌊n/i⌋T(⌊ijn⌋)=O(n3/4)+Oi=2∑⌊n⌋j=2∑⌊n/i⌋T(⌊ijn⌋)
我们考虑 i=2∑⌊n⌋j=2∑⌊n/i⌋T(⌊ijn⌋) 这部分,不难发现:
i=2∑⌊n⌋j=2∑⌊n/i⌋T(⌊ijn⌋)=Ωi=2∑⌊n⌋T(⌊in⋅⌊in⌋−1⌋)=Ωi=2∑⌊n⌋T(⌊in⌋)
由于没有引入记忆化,因此上式中的 T(⌊in⌋) 仍然是 Ω((in)1/4) 的,进而所谓的「高阶无穷小」部分是不可以舍去的.
实际上杜教筛的亚线性时间复杂度是由记忆化保证的.只有使用了记忆化之后才能保证不会出现那个多重求和的项.
例题
问题一
P4213【模板】杜教筛(Sum)
求 S1(n)=∑i=1nμ(i) 和 S2(n)=∑i=1nφ(i) 的值,1≤n<231.
莫比乌斯函数前缀和
我们知道:
ϵ=[n=1]=μ∗1=d∣n∑μ(d)
S1(n)=i=1∑nϵ(i)−i=2∑nS1(⌊in⌋)=1−i=2∑nS1(⌊in⌋)
时间复杂度的推导见 时间复杂度 一节.
对于较大的值,需要用 map/unordered_map 存下其对应的值,方便以后使用时直接使用之前计算的结果.
欧拉函数前缀和
当然也可以用杜教筛求出 φ(x) 的前缀和,但是更好的方法是应用莫比乌斯反演.
莫比乌斯反演
i=1∑nj=1∑n[gcd(i,j)=1]=i=1∑nj=1∑nd∣i,d∣j∑μ(d)=d=1∑nμ(d)⌊dn⌋2
由于题目所求的是 ∑i=1n∑j=1i[gcd(i,j)=1], 所以我们排除掉 i=1,j=1 的情况,并将结果除以 2 即可.
观察到,只需求出莫比乌斯函数的前缀和,就可以快速计算出欧拉函数的前缀和了.时间复杂度 O(n32).
杜教筛
求 S(n)=∑i=1nφ(i).
同样的,φ∗1=id, 从而:
S(n)=i=1∑ni−i=2∑nS(⌊in⌋)=21n(n+1)−i=2∑nS(⌊in⌋)
代码实现
#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) {
if (x < MAXN) return sum_mu[x];
if (mp_mu[x]) return mp_mu[x];
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) {
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++) {
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];
while (T--) {
cin >> n;
cout << S_phi(n) << ' ' << S_mu(n) << '\n';
}
return 0;
}
问题二
「LuoguP3768」简单的数学题
大意:求
i=1∑nj=1∑ni⋅j⋅gcd(i,j)(modp)
其中 n≤1010,5×108≤p≤1.1×109,p 是质数.
利用 φ∗1=id 做莫比乌斯反演化为:
d=1∑nF2(⌊dn⌋)⋅d2φ(d)
其中 F(n)=21n(n+1)
对 ∑d=1nF(⌊dn⌋)2 做数论分块,d2φ(d) 的前缀和用杜教筛处理:
f(n)=n2φ(n)=(id2φ)(n)
S(n)=i=1∑nf(i)=i=1∑n(id2φ)(i)
需要构造积性函数 g,使得 f×g 和 g 能快速求和.
单纯的 φ 的前缀和可以用 φ∗1 的杜教筛处理,但是这里的 f 多了一个 id2,那么我们就卷一个 id2 上去,让它变成常数:
S(n)=i=1∑n((id2φ)∗id2)(i)−i=2∑nid2(i)S(⌊in⌋)
化一下卷积:
((id2φ)∗id2)(i)=d∣i∑(id2φ)(d)id2(di)=d∣i∑d2φ(d)(di)2=d∣i∑i2φ(d)=i2d∣i∑φ(d)=i2(φ∗1)(i)=i3
再化一下 S(n):
S(n)=i=1∑n((id2φ)∗id2)(i)−i=2∑nid2(i)S(⌊in⌋)=i=1∑ni3−i=2∑ni2S(⌊in⌋)=(21n(n+1))2−i=2∑ni2S(⌊in⌋)
分块求解即可.
代码实现
#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) {
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) {
if (k <= pn) return s[k];
if (s_map[k]) return s_map[k];
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);
prime_work(pn);
cout << solve();
return 0;
}
参考资料
- 任之洲,2016,《积性函数求和的几种方法》,2016 年信息学奥林匹克中国国家队候选队员论文
- 杜教筛的时空复杂度分析 - riteme.site