min_25筛
首先拜谢 \(\mathfrak{zhoukangyang}\),感谢他的博客。
本文内容均为誊写他的博客,顺便加入一些自己的理解。
文章中的记号与他的相同,即:
用 \(p\) 表示质数,用 \(P_i\) 表示第 \(i\) 个质数,令 \(P_0=1\)。
简介
min_25 筛可以用来求一类积性函数的前缀和,要求:
\(f(p)\) 是个关于 \(p\) 的项数低的多项式。
\(f(p^k)\) 可以快速求。
大体的思想是通过把索求的函数分为质数和合数两部分分别求解;
质数部分可以通过提前预处理求出;
合数部分可以通过一个递推式,套娃求出。
复杂度瓶颈在于预处理质数部分,为 \(O(\frac{n^{\frac{3}{4}}}{\log n})\)。
Part 1
我们设一个函数 \(S\),定义如下:
然后对这个函数分类讨论:
(1).\(i\) 为质数
我们记函数 \(q\):
即为 \(n\) 以内所有素数的 \(f\) 之和。
此时通过容斥,\(f(n,j)\) 在 \(i\) 为质数处的贡献之和为:
(2).\(i\) 为合数
我们枚举这个合数的最小质因子及其指数。
我们记枚举的最小质因子为 \(P_k\)。
由于 \(S\) 函数只计算最小质因子大于 \(P_j\) 的合数的贡献,故我们枚举的最小质因子 \(P_k\) 应满足 \(P_k > P_j\),即 \(k>j\) 作为下界。
同时,由于这个数是合数,最小也是由 \(P_k^2\) 组成,故我们有 \(P_k^2\le n\) 作为上界。
同时这也揭示了一点:我们只需要用到 \(\sqrt n\) 以内的质数,可以通过线性筛筛出来。
接下来写出贡献之和:
我们解释一下求和里头为什么长这个样子:
注意,我们枚举的这个合数,它里面的 \(P_k\) 一共只有 \(e\) 个。我们记这个合数为 \(P_k^e\times t\),就一定有 \(\gcd(P_k^e,t)=1\)。
我们可以记 \(t\) 的最小质因子为 \(P_T\)。
注意到三点:
-
由于 \(f\) 为积性函数,且 \(\gcd(P_k^e,t)=1\) ,我们就可以考虑把 \(f(P_k^e)\) 提出来,变成 \(f(P_k^e) \times f(t)\)。
-
这个合数的最大值显然为 \(n\),而因为 \(P_k^e\times t\),我们发现 \(t\) 的上界为 \(\dfrac{n}{P_k^e}\)。
-
由于 \(P_k\) 为这个合数的最小质因子,\(t\) 还和 \(P_k^e\) 互质,那么 \(t\) 的最小质因子一定比 \(P_k\) 大,即 \(P_T>P_k\),即 \(T>k\)。
通过这两点,我们差不多可以明白
是怎么个事了,后者的实际含义就是在求和 \(f(t)\) 的贡献!
两个参数中,前者在表达 \(t\) 的上界为 \(\dfrac{n}{P_k^e}\) ,后者在表达 \(t\) 的最小质因子一定比 \(P_k\) 大,即 \(T>k\),也就是我们注意到的第二点和第三点。
这一部分就解释清楚了!
但还有一个东西:
是啥东西?
其实就是在表达当 \(e>1\) 时,\(f(P_k^e)\) 本身的贡献也要算进来。
为什么必须是 \(e>1\)?因为当 \(e=1\) 时,\(f(P_k)\) 就成了质数,在上一个质数部分算过贡献了。
于是我们把 \(i\) 为质数和 \(i\) 为合数的两部分贡献式合并一下,就得到了 \(S\) 的转移式:
可以发现,我们这个题要求的前缀和就是 \(S(n,0) +f(1)=S(n,0) +1\)。
最终我们通过递归实现了这个函数,我们先看看递归过程中会用到什么:
首先,我们有 \(P_j^2\le n\)。这意味着我们筛出 \(\sqrt n\) 以内的所有质数后,求一下质数的 \(f\) 的前缀和,\(\sum\limits_{i=1}^{j}f(P_i)\) 这一部分就搞定了。
而随着递归的深入,我们发现 \(q\) 里头的变量,只有:
这些可能的取值,这是因为:
写了那么多遍数论分块,我们肯定知道这个东西的取值一共有 \(2\sqrt n\) 种。
于是我们只需要处理处 \(q\) 取这 \(2\sqrt n\) 种取值下的值,就能够对 \(S\) 进行转移了!
Part 2
\(q(n)\) 函数是个啥?就是对于 \(n\) 以内所有的质数,它们的 \(f\) 之和。
虽然说 \(f\) 是个积性函数,但它其实是个项数很少的多项式的形式。
假如说 \(f\) 长这样:
我们就可以给它拆成 \(t+1\) 个完全积性函数,为:
进而有 \(f(x)=\sum\limits_{i=1}^{t}f'_i(x)\)。
我们可以分别求出每一个 \(f'\),在 \(n\) 以内所有质数处的取值之和,然后对它们求个和,就能够得到 \(q(n)\) 了!
一定要记住,我们只关注质数处的取值,所以合数处的答案变没变不重要。
至于为什么我们非要把 \(f\) 拆开成 \(f'\),因为 \(f'\) 是完全积性函数,接下来的推导需要完全积性函数的性质。
我们针对某一个 \(f'\) 进行讨论:
我们设一个函数 \(g\):
这里面的符号 \(\mid\) 为或。
这个函数的含义就是:\(n\) 以内所有的质数的 \(f'\) 之和,再加上一部分最小质因子大于 \(P_j\) 的合数的 \(f'\) 之和。
我们注意到过,如果确定了一个合数的最小质因子 \(P_k\),那么这个合数最小为 \(P_k^2\)。
于是我们可以通过取一个合适的 \(j\),使得 \(g(n,j)\) 只包含质数的贡献。
我们可以用 \(g\) 把这个 \(f'\) 对 \(q\) 的贡献表示出来,设为 \(q'(n)\),即:
所以我们想办法把 \(g\) 的转移式搞出来就行了。
我们考虑 \(g(n,j-1)\) 比 \(g(n,j)\) 多出来了哪些贡献:
(1).\(P_j^2>n\)
这个情况下,显然 \(g(n,j-1)\) 和 \(g(n,j)\) 相等,因为这两者都只包含质数的贡献。(手玩一下,发现合数都不会被算进来)
(2).\(P_j^2\le n\)
此时多出来的某些合数 \(x\) 一定含有最小质因子 \(P_j\)。
设 \(x=P_j\times t\)
又由于函数 \(f'\) 是完全积性函数,我们可以考虑把 \(f'(P_j)\) 提出来。
注意到:
-
\(x\) 的上界是 \(n\),所以 \(t\) 的上界为 \(\dfrac{n}{P_j}\)。
-
\(x\) 的最小质因子为 \(P_j\),所以 \(t\) 的最小质因子一定大于等于 \(P_j\),这是下界。
因此我们可以写出多出来的这部分贡献的模样:
里面 \(g\) 就是在枚举 \(t\),而由于 \(P_i\) 为 \(x\) 的最小质因子,所以 \(t\) 的枚举中不能有小于 \(P_i\) 的质数提供贡献,于是我们减去了 \(\sum\limits_{i=1}^{j-1}f'(P_i)\)。
综合以上两部分,我们就能把 \(g\) 的总递推式写出来了:
我们用滚动数组滚掉 \(j\) 这一维,两层 for 循环就能够递推求出 \(g(n,\max\limits_{P_k^2\le n}k)\),即 \(q'(n)\) 了!
然后我们对每个 \(f'\) 都进行一遍上述过程,再把所有的 \(q'(n)\) 求和,就能够求出 \(q(n)\) 了!!
至此,我们所有的处理都弄完了,只需要按照上述过程实现代码即可。
当然,我们还有很多细节,比如说把 \(2\sqrt n\) 个 \(q(n)\) 中 \(n\) 的取值离散化,借助两个神奇的数组 \(id1\) 和 \(id2\),去掉离散化的 \(\log\);
一堆循环的上下界需要处理,之类的一堆细节,看代码吧!
这里附上模板题 P5325 【模板】Min_25 筛 的代码:
点击查看代码
//#pragma GCC optimize(2)
//#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast","inline")
#include<bits/stdc++.h>
#define fr(a) freopen(a,"r",stdin)
#define fw(a) freopen(a,"w",stdout)
#define MP(a,b) make_pair(a,b)
#define DEBUG
using namespace std;
typedef long long ll;
const int N=2e5+10,mod=1e9+7;
const int inv2=500000004,inv6=166666668;
ll n,qn;
ll p[N],tot,v[N];
ll s1[N],s2[N];//质数前缀和&质数平方前缀和
ll m,id1[N],id2[N],w[N];//离散化
ll g1[N],g2[N];
void shai(ll n){
for(int i=2;i<=n;i++){
if(!v[i]){
v[i]=i,p[++tot]=i;
}for(int j=1;j<=tot;j++){
if(p[j] > v[i] || p[j] > n/i) break;
v[i*p[j]]=p[j];
}
}
for(int i=1;i<=tot;i++){
// (s1[i] += p[i]) %=mod;
// (s2[i] += p[i] * p[i] % mod) %=mod;
s1[i] = (s1[i-1] + p[i]) %mod;
s2[i] = (s2[i-1] + p[i] * p[i] % mod) %mod;
}return;
}
ll id(ll x){
if(x <= qn) return id1[x];
else return id2[n/x];
}
ll S(ll n,ll j){
if(p[j] > n) return 0;
ll res = (g2[id(n)] - g1[id(n)])%mod - (s2[j] - s1[j])%mod;
res=(res%mod+mod)%mod;
for(int k=j+1;k<=tot && p[k] * p[k] <= n;k++){
for(ll e=1,now=p[k];now<=n;now*=p[k],e++){
(res += now%mod * (now%mod-1) %mod * (S(n/now,k) + (e>1)) %mod)%=mod;
}
}return res;
}
int main(){
scanf("%lld",&n);qn=sqrt(n);shai(qn);
for(ll l=1,r;l<=n;l=r+1){
r=n/(n/l),w[++m]=n/l;
// printf("l:%lld r:%lld\n",l,r);
g1[m] = w[m]%mod * (w[m]%mod + 1) %mod * inv2 %mod -1;
g2[m] = w[m]%mod * (w[m]%mod + 1) %mod * (2 * w[m]%mod + 1) %mod *inv6 %mod -1;
if(w[m] <= qn) id1[w[m]]=m;
else id2[n/w[m]] = m;
}//离散化
//这里通过滚动数组优化。
for(ll j=1;j<=tot;j++){//j是阶段,我们先枚举j,当然直接枚到tot就行,因为筛的质数最大到根号n
for(ll i=1;i<=m && p[j] * p[j] <= w[i];i++){
//根据转移术式,当pj*pj>wi时,根本就不用变!
(g1[i] -= p[j] * (g1[id(w[i]/p[j])] - s1[j-1] )%mod )%=mod;
(g2[i] -= p[j] * p[j] % mod * (g2[id(w[i]/p[j])] - s2[j-1] )%mod )%=mod;
(g1[i] += mod )%=mod;(g2[i] += mod) %=mod;
}
}
ll ans=S(n,0) + 1;
printf("%lld\n",(ans+mod)%mod);
return 0;
}
//10000000000