【学习笔记】狄利克雷卷积与高级筛法
狄利克雷卷积#
概念#
对于数论函数 ,定义其狄利克雷卷积 ,满足:
运算律:
-
满足交换律,显然具有对称性。
-
满足结合律,等价于三个 贡献到 。
-
满足加法的分配率。
常见数论函数:
-
,常函数,值恒为 。
-
,幂函数,值为 。
-
,狄利克雷卷积的单位元,除 外其余均为 。
-
,莫比乌斯函数,狄利克雷卷积的逆元。
-
,欧拉函数。
-
,除数函数,定义 。
常见数论函数卷积变换#
杜教筛#
原理#
求数论函数前缀和。
设 ,容易推得:
整理一下可以写成:
如果构造出可以快速计算前缀和的函数 ,就可以递归求解了。
时间复杂度#
首先需要证明的是:
设 ,则有:
于是:
从而:
即:
这说明当我们数论分块向下递归时,递归到 ,枚举 得到 ,一定可以表示为 的形式,而这一定在第一层递归时处理到(不考虑先后顺序),于是在记忆化的加持下,计算复杂度只需要考虑第一层递归即可。
设当前块长 ,对复杂度的贡献就是 。
当 时,第二层最大 ,所以总复杂度 。
当 时,就写成:
但较小的前缀和是可以预处理出的,假设预处理出了 的前缀和,那么也就是只有 的部分才需要递归,也就变成:
取 得到理论最优复杂度 。
数论分块时(无论嵌套几层)使用杜教筛求数论函数前缀和,由于所有询问的区间右端点为 ,可以表示为上述记忆化的形式,所以并不会增加复杂度。
点击查看代码
namespace Phi{
int pr[maxn],mn[maxn],mnpw[maxn],phi[maxn];
bool vis[maxn];
ull s1[maxn],s2[maxn];
bool mark[maxn];
inline void linear_sieve(){
mn[1]=1,phi[1]=1;
for(int i=2;i<=lim;++i){
if(!vis[i]) pr[++pr[0]]=i,mn[i]=i,mnpw[i]=i,phi[i]=i-1;
for(int j=1;j<=pr[0]&&i*pr[j]<=lim;++j){
vis[i*pr[j]]=1,mn[i*pr[j]]=pr[j];
if(mn[i]==pr[j]) mnpw[i*pr[j]]=mnpw[i]*pr[j];
else mnpw[i*pr[j]]=pr[j];
if(i*pr[j]==mnpw[i*pr[j]]) phi[i*pr[j]]=phi[i]*pr[j];
else phi[i*pr[j]]=phi[i*pr[j]/mnpw[i*pr[j]]]*phi[mnpw[i*pr[j]]];
if(i%pr[j]==0) break;
}
}
for(int i=1;i<=lim;++i) s1[i]=s1[i-1]+phi[i];
}
ull get_sum(int N){
if(N<=lim) return s1[N];
if(mark[n/N]) return s2[n/N];
mark[n/N]=1;
ull sum=0;
for(unsigned int l=2,r;l<=N;l=r+1){
r=N/(N/l);
sum+=1ull*(r-l+1)*get_sum(N/l);
}
if(N&1) sum=1ull*(N+1)/2*N-sum;
else sum=1ull*N/2*(N+1)-sum;
return s2[n/N]=sum;
}
inline ull solve(){
memset(mark,0,sizeof(mark));
return get_sum(n);
}
}
namespace Mu{
int pr[maxn],mu[maxn];
bool vis[maxn];
int s1[maxn],s2[maxn];
bool mark[maxn];
inline void linear_sieve(){
mu[1]=1;
for(int i=2;i<=lim;++i){
if(!vis[i]) pr[++pr[0]]=i,mu[i]=-1;
for(int j=1;j<=pr[0]&&i*pr[j]<=lim;++j){
vis[i*pr[j]]=1,mu[i*pr[j]]=-mu[i];
if(i%pr[j]==0){
mu[i*pr[j]]=0;
break;
}
}
}
for(int i=1;i<=lim;++i) s1[i]=s1[i-1]+mu[i];
}
int get_sum(int N){
if(N<=lim) return s1[N];
if(mark[n/N]) return s2[n/N];
mark[n/N]=1;
ull sum=0;
for(unsigned int l=2,r;l<=N;l=r+1){
r=N/(N/l);
sum+=(r-l+1)*get_sum(N/l);
}
sum=1-sum;
return s2[n/N]=sum;
}
inline int solve(){
memset(mark,0,sizeof(mark));
return get_sum(n);
}
}
常用构造#
最常用的是 以及 。
在此基础上有:,。
另外根据 的定义有 。
例题#
Luogu-P5218 无聊的水题 II#
容易发现是要求选出一个集合满足 。
设 为值域 的方案数,那么 就是 ,可以单步容斥。
式子长得像杜教筛,复杂度 不太好过。
考虑设 为 的方案数, 为 的方案数,则 ,且有:
根据莫比乌斯反演有:
所以:
杜教筛预处理+数论分块即可,使用光速幂复杂度 。
LibreOJ-6229 这是一道简单的数学题#
把 的上下界改成 ,处理一下 的情况即可。
简单反演得到:
是 次自然数幂和。
于是要求 的前缀和,不难发现这个函数是 ,根据狄利克雷卷积的交换律以及结合律,可以得到 。
于是有:
杜教筛处理即可。
LibreOJ-6491 XXOI2018 简单的最大公约数#
和上面的题差不多,先设 表示值域 , 个数 的方案数,答案是 ,最终求解用数论分块。
直接算还是 。
希望预处理一些 。
依旧是莫比乌斯反演,把 扩充到值域 , 的方案数, 定义为值域 , 的方案数,于是反演得到:
只关心 ,所以有:
这个直接求一行比较困难,考虑差分:
分析一下什么时候 ,显然一定是 ,即 恰好到了下一个块里。
于是这个预处理就是调和级数 的,由于 的性质,实际产生贡献的会更少。
这样大致处理 左右,再套类似杜教筛的东西就可以通过了。
另一个做法是使用欧拉反演,即利用 的性质,可以得到:
杜教筛朴素计算即可。
PN 筛#
Powerful Number#
定义一个数 是 Powerful Number(PN) 当且仅当 。
每个 PN 一定可以被唯一表示为 的形式,次数为偶数的放在 部分,次数为奇数的将 次放在 部分,剩余放在 部分。
于是可以通过枚举 的值来计算 PN 的个数,大致是:
所以可以通过 DFS 来得出所有 PN。
求积性函数前缀和#
以 模板题 为例。
给定积性函数 ,求其前缀和。
考虑构造积性函数 ,满足 ,这里 ,可以构造 。
再找到积性函数 ,满足 ,由于 ,积性函数 处点值一定为 ,所以 ,即 ,这样所有可以被质因子筛到的点值处都是 ,也就是所有非 PN 点值都是 。
这样化简 前缀和的式子:
的前缀和可以用杜教筛求出(有更优秀的也可以),计算答案在 DFS 求出所有 PN 时进行,其中求 可以求出所有 再累乘。
求 最常规的办法是利用 ,移项得到 。
点击查看代码
ll n;
int pr[maptrn],phi[maptrn];
bool vis[maptrn];
int s1[maptrn],s2[maptrn];
bool mark[maptrn];
int pw[maptrn/10][40],h[maptrn/10][40];
inline void linear_sieve(){
phi[1]=1;
for(int i=2;i<=lim;++i){
if(!vis[i]) pr[++pr[0]]=i,phi[i]=i-1;
for(int j=1;j<=pr[0]&&i*pr[j]<=lim;++j){
vis[i*pr[j]]=1,phi[i*pr[j]]=phi[i]*phi[pr[j]];
if(i%pr[j]==0){
phi[i*pr[j]]=phi[i]*pr[j];
break;
}
}
}
for(int i=1;i<=lim;++i) s1[i]=(s1[i-1]+1ll*i*phi[i]%mod)%mod;
for(int i=1;i<=pr[0];++i){
pw[i][0]=1,h[i][0]=1;
for(ll j=pr[i],e=1;;j*=pr[i],++e){
pw[i][e]=j%mod;
int now=1ll*pw[i][e]*(pw[i][e]-1)%mod;
for(int k=0;k<e;++k){
now=(now-1ll*h[i][k]*(pr[i]-1)%mod*pw[i][e-k-1]%mod*pw[i][e-k]%mod+mod)%mod;
}
h[i][e]=now;
if(j>n/pr[i]) break;
}
}
}
int get_sumg(ll N){
if(N<=lim) return s1[N];
if(mark[n/N]) return s2[n/N];
mark[n/N]=1;
int tmpn=N%mod;
int res=1ll*tmpn*(tmpn+1)%mod*(2ll*tmpn%mod+1)%mod*inv6%mod;
for(ll l=2,r;l<=N;l=r+1){
r=N/(N/l);
int tmpl=(l-1)%mod,tmpr=r%mod;
int now=(1ll*tmpr*(tmpr+1)/2-1ll*tmpl*(tmpl+1)/2)%mod;
res=(res-1ll*now*get_sumg(N/l)%mod+mod)%mod;
}
return s2[n/N]=res;
}
int ans;
void dfs(ll N,int ptr,int res){
ans=(ans+1ll*res*get_sumg(n/N)%mod)%mod;
if(ptr>pr[0]) return;
for(int i=ptr;i<=pr[0];++i){
if(N>n/(1ll*pr[i]*pr[i])) break;
for(ll j=N*pr[i]*pr[i],e=2;;j*=pr[i],++e){
dfs(j,i+1,1ll*res*h[i][e]%mod);
if(j>n/pr[i]) break;
}
}
}
int main(){
scanf("%lld",&n);
linear_sieve();
dfs(1,1,1);
printf("%d\n",ans);
return 0;
}
复杂度分析#
求 前缀和复杂度认为是杜教筛复杂度 。
只预处理 的质数,每个质数枚举 并计算 的复杂度是 ,素数密度 ,所以总复杂度是 ,实际这个上界比较松。
于是得到大致是 的复杂度。
算法流程总结#
-
用积性函数拟合出合适的 。
-
构造快速求 前缀和的方法。
-
预处理出 。
-
DFS 出所有 PN 并计算答案。
例题#
LibreOJ-6053 简单的函数#
除了 以外的 处点值是 ,先不考虑特例的话拟合成 是非常好的,在此基础上 ,所以将 的倍数处拟合成 。(肯定不是 吧)
把 提出一个会好一点,顺便把系数也带出来:
于是 ,也即 。
注意到 有意义的 都是偶数,可以用一个 来代替,即 。
把 详写成关于 的:
参照上面的化简方法,得到:
这个和 一模一样,这里当然再把 换成 。
放回最初的式子 ,这样一直把 展开,就是一个倍增的形式,单次计算是 ,所求点值都在杜教筛射程范围内。
构造的另一函数 就是朴素的 预处理。
根据 PN 的数量级和预处理的复杂度,总体仍是 。
Min_25 筛#
Min_25 筛是一种基于埃筛的筛法,可以对数论函数 求前缀和,要求 是关于 的低次多项式,且 可快速计算。
构造质数点值前缀和函数#
在求解的开始,先用线性筛筛出小于等于 的质数,定义 表示 的最小质因子。
设 ,这个式子不好直接求,先设 ,即 ,其中 是 这个关于 的低次多项式 次项系数。
设 ,这个定义相当于埃筛筛掉了含小于等于 质因子的合数,容易发现找到最小的质数 满足 ,那么 ,因为此时不再有最小值因子大于 的合数。
考虑递推,有:
第二个递推式实际上就是把 的数拆成 和另一部分的乘积,这一部分就表现在 中,但这里面还包含了质数,所以要减去。
于是只关心 个块筛位置的点值,而 ,所以一定可以被块筛筛到,这样计算复杂度也是经典方法了,具体是:
构造前缀和函数#
方法一:递归#
设 ,仿照上面,扩充 的定义为 ,这样答案就是 。
直接递归,式子是:
就是分质数合数来计算,后半部分相当于枚举了一个最小值因子及幂次,然后递归求解, 的加入源于 并不计算 的点值,因此要补充计算,而 已经在质数处算过了。
复杂度证明见论文,结论是复杂度为 ,但 时为 。
实现常数很小,但只能求单点。
点击查看代码
inline int S1(ll N){
N%=mod;
return N*(N+1)%mod*inv2%mod;
}
inline int S2(ll N){
N%=mod;
return N*(N+1)%mod*(2*N+1)%mod*inv6%mod;
}
inline int f(ll N){
N%=mod;
return N*(N-1+mod)%mod;
}
ll n;
int lim;
int pr[maxn],cntpr;
bool vis[maxn];
ll id[maxn<<1];
int id1[maxn],id2[maxn];
int g1[maxn<<1],g2[maxn<<1];
inline int get_id(ll N){
if(N<=lim) return id1[N];
else return id2[n/N];
}
inline void linear_sieve(){
for(int i=2;i<=lim;++i){
if(!vis[i]) pr[++cntpr]=i;
for(int j=1;j<=cntpr&&i*pr[j]<=lim;++j){
vis[i*pr[j]]=1;
if(i%pr[j]==0) break;
}
}
}
int S(ll N,int j){
if(pr[j]>=N) return 0;
int res=0;
res=(g1[get_id(N)]-g1[get_id(pr[j])]+mod)%mod;
for(int k=j+1;k<=cntpr&&1ll*pr[k]*pr[k]<=N;++k){
ll now=pr[k];
for(int e=1;now<=N;++e,now*=pr[k]){
res=(res+1ll*f(now)*(S(N/now,k)+(e>1))%mod)%mod;
}
}
return res;
}
int main(){
scanf("%lld",&n);
lim=sqrt(n)+1;
linear_sieve();
for(ll l=1,r;l<=n;l=r+1){
r=n/(n/l);
id[++id[0]]=n/l;
if(n/l<=lim) id1[n/l]=id[0];
else id2[n/(n/l)]=id[0];
g1[id[0]]=(S1(n/l)-1+mod)%mod,g2[id[0]]=(S2(n/l)-1+mod)%mod;
}
for(int j=1;j<=cntpr;++j){
for(int i=1;i<=id[0]&&1ll*pr[j]*pr[j]<=id[i];++i){
g1[i]=(g1[i]-1ll*pr[j]*(g1[get_id(id[i]/pr[j])]-g1[get_id(pr[j-1])]+mod)%mod+mod)%mod;
g2[i]=(g2[i]-1ll*pr[j]*pr[j]%mod*(g2[get_id(id[i]/pr[j])]-g2[get_id(pr[j-1])]+mod)%mod+mod)%mod;
}
}
for(int i=1;i<=id[0];++i) g1[i]=(g2[i]-g1[i]+mod)%mod;
printf("%d\n",(S(n,0)+1)%mod);
return 0;
}
方法二:递推#
考虑把 的状态设计和 类比为 。
这样目标是得到 ,初始 (实际上就是只有质数部分,第二维取第一个大于 的质数编号即可)。
递推过程:
这里对 做限制的原因是保证了 ,从而消去 是正确的。
复杂度同上,但常数较大,优越性在于递推算法求出了块筛。
点击查看代码
inline int S1(ll N){
N%=mod;
return N*(N+1)%mod*inv2%mod;
}
inline int S2(ll N){
N%=mod;
return N*(N+1)%mod*(2*N+1)%mod*inv6%mod;
}
inline int f(ll N){
N%=mod;
return N*(N-1+mod)%mod;
}
ll n;
int lim;
int pr[maxn],cntpr;
bool vis[maxn];
ll id[maxn<<1];
int id1[maxn],id2[maxn];
int g1[maxn<<1],g2[maxn<<1];
int S[maxn<<1];
inline int get_id(ll N){
if(N<=lim) return id1[N];
else return id2[n/N];
}
inline void linear_sieve(){
for(int i=2;i<=lim;++i){
if(!vis[i]) pr[++cntpr]=i;
for(int j=1;j<=cntpr&&i*pr[j]<=lim;++j){
vis[i*pr[j]]=1;
if(i%pr[j]==0) break;
}
}
}
int main(){
scanf("%lld",&n);
lim=sqrt(n)+1;
linear_sieve();
for(ll l=1,r;l<=n;l=r+1){
r=n/(n/l);
id[++id[0]]=n/l;
if(n/l<=lim) id1[n/l]=id[0];
else id2[n/(n/l)]=id[0];
g1[id[0]]=(S1(n/l)-1+mod)%mod,g2[id[0]]=(S2(n/l)-1+mod)%mod;
}
for(int j=1;j<=cntpr;++j){
for(int i=1;i<=id[0]&&1ll*pr[j]*pr[j]<=id[i];++i){
g1[i]=(g1[i]-1ll*pr[j]*(g1[get_id(id[i]/pr[j])]-g1[get_id(pr[j-1])]+mod)%mod+mod)%mod;
g2[i]=(g2[i]-1ll*pr[j]*pr[j]%mod*(g2[get_id(id[i]/pr[j])]-g2[get_id(pr[j-1])]+mod)%mod+mod)%mod;
}
}
for(int i=1;i<=id[0];++i) S[i]=g1[i]=(g2[i]-g1[i]+mod)%mod;
for(int j=cntpr;j>=1;--j){
for(int i=1;i<=id[0]&&1ll*pr[j]*pr[j]<=id[i];++i){
ll now=pr[j];
for(int e=1;now*pr[j]<=id[i];++e,now*=pr[j]){
S[i]=(S[i]+1ll*f(now)*(S[get_id(id[i]/now)]-S[get_id(pr[j])]+mod)%mod+f(now*pr[j]))%mod;
}
}
}
printf("%d\n",(S[1]+1)%mod);
return 0;
}
例题#
LibreOJ-6235 区间素数个数#
只需要做 Min_25 的第一步,求 即可。
LibreOJ-6027 from CommonAnt 质数计数 I#
就是求模 余 的质数个数,注意到求 部分是把 分成了 和其他两部分,而 中每个数的余数乘上 的余数才是得到 合数的余数。因此不能只求余数为 的,计算时按余数分组即可。
LibreOJ-6028 from CommonAnt 质数计数 II#
与上一题类似
LibreOJ-6053 简单的函数#
先按 求 ,第二步算之前给前缀和再加上 即可。
注意第一步只需要保证完全积性,第二步只需要保证积性,所以这样处理是正确的。
LibreOJ-6785 简单的函数 10^13 版#
要求块筛,用递推方法,需要大量卡常。
LibreOJ-6181 某个套路求和题#
不是积性函数,但是很简洁,容易发现质数位置是 ,含平方因子位置是 。
不含平方因子的合数位置,设共 个质因子,那么只关心 的位置,也就是 中选出奇数个,方案数 ,因此不含平方因子的合数位置是 。
只看合数位置可以拟合成 ,这样质数位置从 变成了 ,答案就应该是:
前一个式子直接 Min_25 前缀和即可,容易发现后一个式子等价于第一步的 函数。
参考资料#
狄利克雷卷积#
杜教筛#
-
OI Wiki
PN 筛#
-
OI Wiki
Min_25 筛#
作者:SoyTony
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效