杜教筛及相关知识小结
huxuanrui19 · · 算法·理论
制作不易,可以点赞收藏,以便复习。
upd on 2026/4/6: 杜教筛时间复杂度证明有误,已修改。
求通过。
切入正题
一切都来源于它。
杜教筛被用于处理一类数论函数的前缀和问题。对于数论函数
f ,杜教筛可以在低于线性时间的复杂度内计算——以上内容来自 [oi_wiki](https://oi-wiki.org/math/number-theory/du/)。 # 1. 前置知识 ## 1.1 积性函数 对于一个定义域为所有正整数的函数(数论函数),若满足 $\forall p,q\in N^*$ 且 $\gcd(p,q)=1$ 时,有 $f(pq)=f(p)f(q)$,则称此数论函数为积性函数。 特别的,若 $\forall p,q\in N^*$,都有 $f(pq)=f(p)f(q)$,则称此数论函数为**完全**积性函数。 这里举一些栗子,
比如
单位函数
其中
1.2 数论分块(整除分块)
这是为了解决一类求和问题:
这里如果我们打出表格(以 8 为例)
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 8/i | 8 | 4 | 2 | 2 | 1 | 1 | 1 | 1 |
会发现有一些
证明如下
代码如下
int ans=0;
for(int l=1,r;l<=n;l=r+1){
r=n/(n/l);
ans+=(r-l+1)*(n/l);
}
这样我们分块的数量小于
也简单证明一下
1.3 狄利克雷卷积
设
注意:
1.狄利克雷卷积满足交换律和结合律。
2.
3.特别的,对于任意数论函数
4.两个积性函数的狄利克雷卷积仍是积性函数。
1.4 莫比乌斯函数&莫比乌斯反演
先给出莫比乌斯函数的定义。
莫比乌斯函数也是一个积性函数,特别的
接下来就可以给出莫比乌斯反演公式了。
若
所以设
注意莫比乌斯反演不需要
2.例题
2.1 P2261 余数求和
题目传送门。
题目大意
求
思路
先给出一个有用的公式(用
这个还是显然的。
则原式可化为:
前一部分直接算,后一部分用整除分块。
注意
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){
ll n,k,ans=0;
cin>>n>>k;
ans=n*k;
for(ll l=1,r;l<=n;l=r+1){
if(k/l) r=min(k/(k/l),n); //不为0时
else r=n; //等于0时
ans-=(k/l)*(l+r)*(r-l+1)/2;
}
cout<<ans;
return 0;
}
2.2 P2424 约数和
题目传送门。
题目描述
定义
求
思路
还是推式子。
记
考虑
这里运用到一个很重要的思想,就是枚举一堆数的因数,和枚举因数,在这堆数中枚举倍数的效果是一样的。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll sum(ll n){
ll ans=0;
for(ll l=1,r;l<=n;l=r+1){
r=n/(n/l);
ans+=(l+r)*(r-l+1)/2*(n/l);
}
return ans;
}
int main(){
ll x,y;
cin>>x>>y;
cout<<sum(y)-sum(x-1);
return 0;
}
这里还有一个类似的题目,只不过求的是因数个数函数。
题目传送门 P9611。
2.3 P3455 [POI 2007] ZAP-Queries
题目描述
求
思路
又又又推式子。
记
运用
只要求出
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e4+10;
ll cnt,mu[N],vis[N],pri[N],sum[N];
void init()
{
mu[1]=1;
for(int i=2;i<N;i++){
if(vis[i]==0){
mu[i]=-1;
vis[i]=1;
pri[++cnt]=i;
}
for(int j=1;j<=cnt;j++){
if(i*pri[j]>=N){
break;
}
vis[i*pri[j]]=1;
if(i%pri[j]==0){
mu[i*pri[j]]=0;
break;
}
mu[i*pri[j]]=mu[i]*mu[pri[j]];
}
}
sum[0]=0;
sum[1]=mu[1];
for(int i=2;i<N;i++){
sum[i]=sum[i-1]+mu[i];
}
} //线性筛求莫比乌斯函数
ll kkk(ll a,ll b,ll d){
a/=d;
b/=d;
ll ans=0,r=0;
if(a<b) swap(a,b);
for(ll l=1;l<=b;l=r+1){
r=min(a/(a/l),b/(b/l));
ans+=(sum[r]-sum[l-1])*(long long)(a/l)*(b/l);
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
init();
int t;
cin>>t;
while(t--){
ll a,b,d;
cin>>a>>b>>d;
cout<<kkk(a,b,d)<<endl;
}
return 0;
}
这题有个类似的 P4450,还有一个扩展 P2257
简单讲一下扩展的思路:
令小于
令
后面这一部分可以再
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e7+10;
ll cnt,mu[N],vis[N],pri[N],f[N],sum[N];
void init()
{
mu[1]=1;
for(int i=2;i<N;i++){
if(vis[i]==0){
mu[i]=-1;
vis[i]=1;
pri[++cnt]=i;
}
for(int j=1;j<=cnt;j++){
if(i*pri[j]>=N){
break;
}
vis[i*pri[j]]=1;
if(i%pri[j]==0){
mu[i*pri[j]]=0;
break;
}
mu[i*pri[j]]=mu[i]*mu[pri[j]];
}
}
for(int i=1;i<=cnt;i++){
for(int j=1;j*pri[i]<N;j++){
f[pri[i]*j]+=mu[j];
}
}
sum[0]=0;
sum[1]=f[1];
for(int i=2;i<N;i++){
sum[i]=sum[i-1]+f[i];
}
}
ll kkk(ll a,ll b){
ll ans=0,r=0;
if(a<b) swap(a,b);
for(ll l=1;l<=b;l=r+1){
r=min(a/(a/l),b/(b/l));
ans+=(sum[r]-sum[l-1])*(long long)(a/l)*(b/l);
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
init();
int t;
cin>>t;
while(t--){
ll a,b;
cin>>a>>b;
cout<<kkk(a,b)<<'\n';
}
return 0;
}
2.4 P5221 Product
题目传送门。
题目描述
求
浅浅退亿个式子:
对于前半部分,有:
对于后半部分,有
对于上面的式子就是 P2158 的故事
整合一下就 OK 了。
这里没有用莫比乌斯反演,只是帮大家感受一下连乘的推式子题。
注意这题的时空限制,所以对指数要对
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
const ll mod=104857601;
int cnt,phi[N],pri[N];
bool vis[N];
void sieve(){
phi[1]=1;vis[1]=1;
for(int i=2;i<N;i++){
if(!vis[i]){
phi[i]=i-1;
pri[++cnt]=i;
}
for(int j=1;j<=cnt&&i*pri[j]<N;j++){
vis[i*pri[j]]=1;
if(i%pri[j]==0){
phi[i*pri[j]]=phi[i]*pri[j];
break;
}
phi[i*pri[j]]=phi[i]*(pri[j]-1);
}
}
for(int i=1;i<N;i++){
phi[i]=(phi[i]+phi[i-1])%(mod-1);
}
}
inline ll qp(ll a,ll b){
ll sum=1;
while(b){
if(b&1) sum=sum*a%mod;
a=a*a%mod;
b>>=1;
}
return sum;
}
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
sieve();
ll n;
cin>>n;
ll ans1=1,ans2=1;
for(int i=1;i<=n;i++){
ans1=ans1*i%mod;
ans2=ans2*qp(i,phi[n/i]*2-1)%mod;
}
cout<<(qp(ans1,2*n)*qp(ans2,(mod-2)*2)%mod);
return 0;
}
P7486 「Stoi2031」彩虹
题目传送门。
非常毒瘤的一道题。
题目描述
求
思路
准备好纸笔,一起让暴风雨来得更猛烈些吧。
令
所以原式
接下来就是比较毒瘤的过程,这有一篇视频很好。
还有一篇好不容易才找到的正确题解。
这里直接给出 code(注意本题卡常)。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
const ll mod=32465177;
ll t,n;
template < typename T >
void read(T &x){
x=0;int f=1;char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f*=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<1)+(x<<3)+(ch&15);
ch=getchar();
}
x*=f;
}
void write(int x)
{
if(x<0)
putchar('-'),x=-x;
if(x>9)
write(x/10);
putchar(x%10+'0');
return;
}
inline ll qp(ll a,ll b){
ll sum=1;
while(b){
if(b&1) sum=sum*a%mod;
a=a*a%mod;
b>>=1;
}
return sum;
}
ll cnt,mu[N],vis[N],pri[N],f[N],finv[N],fprod[N],sum[N],x1[N],x2[N],g[N],ginv[N],sumx1[N];
inline void pre(){
mu[1]=1;
vis[1]=1;
for(int i=2;i<=n;i++){
if(!vis[i]){
mu[i]=-1;
pri[++cnt]=i;
}
for(int j=1;j<=cnt&&i*pri[j]<=n;j++){
vis[i*pri[j]]=1;
if(i%pri[j]==0){
mu[i*pri[j]]=0;
break;
}
mu[i*pri[j]]=-mu[i];
}
}
fprod[0]=1;
for(int i=1;i<=n;i++){
f[i]=qp(i,i);
finv[i]=qp(f[i],mod-2);
fprod[i]=fprod[i-1]*f[i]%mod;
sum[i]=1ll*i*(i+1)/2;
x2[i]=1;
}
for(ll i=1;i<=n;i++){
for(ll j=1;j*i<=n;j++){
x1[i*j]=((x1[i*j]+i*mu[i])%(mod-1)+mod-1)%(mod-1);
if(mu[i]>0){
x2[i*j]=x2[i*j]*f[i]%mod;
}
else if(mu[i]<0){
x2[i*j]=x2[i*j]*finv[i]%mod;
}
}
}
g[0]=ginv[0]=1;
for(int i=1;i<=n;i++){
sumx1[i]=(sumx1[i-1]+i*x1[i])%(mod-1);
g[i]=g[i-1]*qp(qp(i,x1[i])*x2[i]%mod,i)%mod;
ginv[i]=qp(g[i],mod-2);
}
}
inline ll yy(ll n,ll m){
return qp(fprod[n],sum[m]%(mod-1))*qp(fprod[m],sum[n]%(mod-1))%mod;
}
inline ll s(ll n,ll m){
ll ans=1;
if(n>=m) swap(n,m);
for(ll l=1,r;l<=n;l=r+1){
r=min(n/(n/l),m/(m/l));
ans=ans*(qp(yy(n/l,m/l),((sumx1[r]-sumx1[l-1])%(mod-1)+mod-1)%(mod-1))*qp(g[r]*ginv[l-1]%mod,(__int128)sum[n/l]*sum[m/l]%(mod-1))%mod)%mod;
}
return ans;
}
inline ll solve(ll l,ll r){
ll ans1=s(r,r);
ll ans2=s(l-1,l-1);
ll ans3=s(l-1,r);
return (ans1*ans2%mod)*qp(ans3,2*(mod-2))%mod;
}
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
read(t);read(n);
pre();
while(t--){
int l,r;
read(l);read(r);
write(solve(l,r));
putchar('\n');
}
return 0;
}
还有一道毒瘤 P5518,大家可以踊跃挑战。
3. 杜教筛介绍
3.1 推导
接下来就是 无聊 有趣的推式子。
问题的原型是这样的,设
为了使其低于线性时间复杂度,尝试构造形如
构造两个积性函数
由卷积的定义得
最后一步得到
这就是杜教筛公式。
3.2 实现
我们可以用递归来实现一个朴素版本(伪代码):
typedef long long ll;
ll getsum(ll n){
ll ans=sumh(n); //h 的前缀和
for(int l=2,r;l<=n;l=r+1){
r=n/(n/l);
ans-=(sumg(r)-sumg(l-1))*(getsum(n/l)); //整除分块
}
return ans;
}
我们会发现这版代码时间复杂度为
但我们发现其实还可以优化,比如用线性筛预处理一部分,还有在递归时加入记忆化。
先以此题为例,有特殊性质
对于
对于
代码用 unordered_map 实现如下
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e6+10;
ll mu[N],phi[N],vis[N],pri[N];
unordered_map<ll,ll> sumphi;
unordered_map<ll,ll> summu;
void init()
{
vis[0]=vis[1]=1;
mu[1]=phi[1]=1;
int cnt=0;
for(int i=2;i<N;i++){
if(vis[i]==0){
phi[i]=i-1;
mu[i]=-1;
vis[i]=1;
pri[++cnt]=i;
}
for(int j=1;j<=cnt;j++){
if(i*pri[j]>=N){
break;
}
vis[i*pri[j]]=1;
if(i%pri[j]==0){
phi[i*pri[j]]=phi[i]*pri[j];
mu[i*pri[j]]=0;
break;
}
phi[i*pri[j]]=phi[i]*phi[pri[j]];
mu[i*pri[j]]=-mu[i];
}
}
for(int i=1;i<N;i++){
phi[i]+=phi[i-1];
mu[i]+=mu[i-1];
}
}
ll getsphi(ll x){
if(x<N){
return phi[x];
}
else if(sumphi.count(x)){
return sumphi[x];
}
ll ans=x*(x+1)/2;
for(ll l=2,r;l<=x;l=r+1){
r=x/(x/l);
ans-=(r-l+1)*getsphi(x/l);
}
sumphi[x]=ans;
return ans;
}//杜教筛
ll getsmu(ll x){
if(x<N){
return mu[x];
}
else if(summu.count(x)){
return summu[x];
}
ll ans=1;
for(ll l=2,r;l<=x;l=r+1){
r=x/(x/l);
ans-=(r-l+1)*getsmu(x/l);
}
summu[x]=ans;
return ans;
}//杜教筛
int main(){
init();
int t;
cin>>t;
while(t--){
ll n;
cin>>n;
cout<<getsphi(n)<<" "<<getsmu(n)<<endl;
}
return 0;
}
不过这版代码有个小问题,就是 unordered_map 会有一点常数。所以我们可以直接建一个长度为
改进如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e6+10,M=1<<17;
ll n,mu[N],phi[N],vis[N],pri[N],sumphi[M],summu[M];
ll ind(ll x){
ll t=sqrt(n);
if(x<=t) return x;
if(x>t) return t+n/x;
}
void init()
{
vis[0]=vis[1]=1;
mu[1]=phi[1]=1;
int cnt=0;
for(int i=2;i<N;i++){
if(vis[i]==0){
phi[i]=i-1;
mu[i]=-1;
vis[i]=1;
pri[++cnt]=i;
}
for(int j=1;j<=cnt;j++){
if(i*pri[j]>=N){
break;
}
vis[i*pri[j]]=1;
if(i%pri[j]==0){
phi[i*pri[j]]=phi[i]*pri[j];
mu[i*pri[j]]=0;
break;
}
phi[i*pri[j]]=phi[i]*phi[pri[j]];
mu[i*pri[j]]=-mu[i];
}
}
for(int i=1;i<N;i++){
phi[i]+=phi[i-1];
mu[i]+=mu[i-1];
}
}
ll getsphi(ll x){
if(x<N){
return phi[x];
}
else if(sumphi[ind(x)]!=-1){
return sumphi[ind(x)];
}
\\杜教筛略去
}
ll getsmu(ll x){
if(x<N){
return mu[x];
}
else if(summu[ind(x)]!=-1){
return summu[ind(x)];
}
\\杜教筛略去
}
int main(){
init();
int t;
cin>>t;
while(t--){
memset(summu,-1,sizeof(summu));
memset(sumphi,-1,sizeof(sumphi)); //记得清空
cin>>n;
cout<<getsphi(n)<<" "<<getsmu(n)<<endl;
}
return 0;
}
这样时间复杂度是
3.3 时间复杂度证明(过于比较复杂,可略过)
回到公式
如果函数
设计算
展开第一层,有
在展开一层,有
第二项是高阶小量,可以直接省略。
回带可以得到
可以看出最后一项最大,只看它就行了。
于是就有
:::warning[错误原因(引自 OI Wiki)]
在“视为高阶无穷小量”时,这里的论断过于突然,我们仔细分析一下,将
我们考虑
由于没有引入记忆化,因此上式中的
实际上杜教筛的亚线性时间复杂度是由记忆化保证的。只有使用了记忆化之后才能保证不会出现那个多重求和的项。
| ::: |
|---|
| ::::success[正确证明(引用自 OI Wiki)] |
| 令 |
若我们可以预处理出一部分
| 若 |
|---|
所以该算法的关键就是要找到合适的
比如求解的函数由形如
3.4 应用
建议大家自己推一遍式子,感受其中的“杜教筛变换”。
然后大家可以完成 P3768。
有些莫比乌斯反演的推式子题可能会要求在低于线性时间复杂度内求出莫比乌斯函数前缀和,这时就可以用杜教筛。
3.4 例题
P3768:
这里令