浅谈数论函数求和法:杜教筛
0. 前言
杜教筛是一种常用的数论函数求和方法。
本文尝试介绍如下内容:
-
基于块筛结构,更清晰地呈现杜教筛的核心思想与算法流程;
-
给出常数优秀的、非递归式的杜教筛实现;
-
给出杜教筛复杂度的正确证明;
-
网上已经有一种错误证法广泛流传,将指出它为什么错。
1. Dirichlet 双曲线法
对于数论函数
我们常要解决的一类问题是:对于两个数论函数
可引入 Dirichlet 双曲线法对其进行快速计算:
若
x,y>0 且xy=n ,则\displaystyle\sum_{ij\le n}f(i)g(j)=\sum_{i\le x}f(i){\rm S}g(n/i)+\sum_{j\le y}g(j){\rm S}f(n/j)-{\rm S}f(x){\rm S}g(y) (这里用
/ 表示整除)
证明是简单的:为计算左式,考虑分别计算
通常取
2. 块筛
对于数论函数
块筛包含了所有
回顾前文 Dirichlet 双曲线法的式子,可以发现,计算其中右式时,我们需要用到的就是
换言之,Dirichlet 双曲线法的作用在于:
已知
3. 块筛卷积
考虑如下问题:
假设
f*g=h ,且f,g 的块筛已知,试求h 的块筛。
使用 Dirichlet 双曲线法,直接对
注意到经典性质:对正整数
这保证了
也即
4. 杜教筛
再考虑如下问题:
假设
f*g=h ,且g,h 的块筛已知,试求{\rm S}f(n) 。
考察 Dirichlet 双曲线法给出的式子:
这里
容易发现求解
为此,考虑如下递推流程:
-
枚举
k=1,2,\cdots,\lfloor\sqrt n\rfloor ,依次求解{\rm S}f(k) ; -
枚举
k=\lfloor\sqrt n\rfloor,\cdots,2,1 ,依次求解{\rm S}f(n/k) 。
这样就求出了
5. 复杂度分析
上述两个问题的算法时间复杂度均为
实际上,对于规模较小的部分往往有更好的方法计算。
设定阈值
-
暴力计算 Dirichlet 卷积,复杂度为
O(B\log B) ; -
使用 Dirichlet 前缀和技术,复杂度为
O(B\log\log B) ; -
直接用线性筛预处理,复杂度为
O(B) 。
剩余部分仍用杜教筛计算,其复杂度变为
假设
取
此时的空间复杂度也为
6. 实现
以洛谷P4213 【模板】杜教筛为例:要求
构造卷积式
再注意到
具体实现可以参考下方代码。
#include<bits/stdc++.h>
#define rep(i,l,r) for (int i=l; i<=r; ++i)
using namespace std;
typedef long long i64;
const int N=2e6,M=5e4;
int T,mx,k,n[12],p[160000];
bitset<N>v;
int mu[N],smu[N],Smu[M];
void init(int n) { //线性筛预处理
mu[1]=1;
rep(i,2,n) {
if (!v[i]) mu[i]=-1,p[++k]=i;
for (int j=1,t; j<=k&&(t=i*p[j])<=n; ++j) {
v[t]=1;
if (i%p[j]==0) break;
mu[t]=-mu[i];
}
}
rep(i,1,n) smu[i]=smu[i-1]+mu[i];
}
inline i64 S(int x) { return x*(x+1ll)>>1; }
void solve(int n) {
int sq=sqrt(n);
for (int i=n/mx+1; i<=sq; ++i) Smu[i]=smu[n/i];
for (int i=n/mx; i; --i) { //杜教筛
int m=n/i,B=sqrt(m);
int s=1+smu[B]*B-m,t,t2;
rep(j,2,B)
t=i*j,t2=m/j,
s-=(t>sq?smu[t2]:Smu[t])+t2*mu[j];
Smu[i]=s;
}
i64 Sphi=-S(sq)*smu[sq];
rep(i,1,sq) Sphi+=1ll*i*Smu[i]+mu[i]*S(n/i);
cout<<Sphi<<' '<<Smu[1]<<'\n';
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cin>>T;
rep(i,1,T) cin>>n[i];
mx=pow(*max_element(n+1,n+T+1),2./3);
init(mx);
rep(i,1,T) solve(n[i]);
return 0;
}
7. 另一种复杂度分析的勘误
此证法认为,杜教筛的复杂度由下式给出:
并由此得到:
这里有一步所谓“舍去高阶小量”的处理,实际上是错误的。
下面用简单的放缩来说明
当
假设
从而可归纳证得
即得
对于杜教筛的递归式实现的复杂度,正确的分析是:
-
应当注意到这一实现是带记忆化的。
这保证了递归过程中每个{\rm S}f(n/i) 只被计算一次,与递推式实现等价。
因此两种实现具有相同的复杂度,即O(n^{\frac34}) 。 -
若不带记忆化,我们已经证明这时的复杂度
T(n)=\Omega(n) ,不符预期。
相比之下,递推式实现的复杂度更为直观,并且常数也较优秀,可以认为是更好的实现方法。
8. 后记
参考文献:negiizhao - OI中常用数论函数求和法的简化陈述
感谢 @渐变色 提供的帮助和指导。