前言
原本以为会了杜教筛就能走遍积性函数题的天下都不怕的了,结果5月省赛时遇到了专门卡杜教筛的题,只好滚回去学了…结果省赛之后一直在忙这忙那的,然后一拖拖到了暑假的现在才有时间来补Min-25筛的内容。
复杂度
O
(
n
3
4
log
n
)
O(\frac{n^\frac{3}{4}}{\log n})
O(lognn43),而且据说常数非常优秀
证明?那是什么,能吃吗?
相关链接
Min-25筛原理:
Min25筛小结
min25筛学习笔记
【学习笔记】Min25筛
模板改编自:
LibreOJ 6053 简单的函数(Min25筛)
min25筛学习笔记
注意
正如在上面的链接中所提到的,Min-25筛使用条件是:
- f ( x ) f(x) f(x) 在 x ∈ p r i m e x \in prime x∈prime 的时候要能够用多项式表示,如 f ( p ) = p 2 + p f(p)=p^2+p f(p)=p2+p
- f ( x k ) f(x^k) f(xk) 在 x ∈ p r i m e x \in prime x∈prime 时要能够快速算出
关于第一点,之所以要求其能用多项式表示,是为了能把其写成多个完全积性函数的若干倍的和/差,这样在第一部分 P j 2 ≤ n P_j^2 \leq n Pj2≤n 时才可以把 f ( P j ) f(P_j) f(Pj) 直接提取出来。
关于第二点,则是为了能在第二部分中以 O ( 1 ) O(1) O(1) 复杂度算出 f ( x k ) , x ∈ p r i m e f(x^k),x\in prime f(xk),x∈prime,否则复杂度将变大。
故Min-25筛并不只能作用于积性函数,只要函数 f f f 满足以上两个条件,就可以使用Min-25筛在低于线性的复杂度下求其前缀和。(比如2020年CCPC网络赛中有一题就是用Min-25筛来求质数的前缀和)
在第一部分求多项式的不同次项时,只会求 g ( ⌊ n i ⌋ ) g(\lfloor \frac{n}{i} \rfloor) g(⌊in⌋),因为只有这些值会被用到。虽然这样子能节省一定的时间,但是也因为不同的 n n n 所对应的 ⌊ n i ⌋ \lfloor \frac{n}{i} \rfloor ⌊in⌋ 列表往往很大不同,导致了多组数据时已算出来的结果不能复用,所以每输入一组数据就得重新做一次。
代码
代码的
f
f
f 函数和
s
i
e
v
e
_
g
sieve\_g
sieve_g 函数中的九处 //...是需要根据具体题目要求进行填空的。
//代码计算的是函数f(i)的前n项和
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5 + 10;//2*sqrt(n)的范围
const LL INV2 = 5e8 +4;//2的逆元(可能用到)
const LL INV6 = 166666668;//6的逆元(可能用到)
const LL MOD = 1e9 + 7;
bool isp[N];
LL n,m,sz,sqrt_n,c0,c1,p[N],w[N],id0[N],id1[N],g0[N],g1[N],sum0[N],sum1[N];
//n是输入的数, sqrt_n保存sqrt(n),即预处理的数量
//sz是质数个数, isp[i]表示i是否质数, p[i]存储第i个质数
//sum0[i]存储的是前i个质数的f0值之和,sum1[i]存储的是前i个质数的f1值之和
//m是n/k的个数, w[i]存储n/k第i种值(倒序), id0和id1[i]存储i这个值在w[i]中的下标
//g0[i]和g1[i]等分别存储f在取质数时的多项式中的不同次方项(此处只有两个数组,即假设题目中的f取质数时只有两项), g0[i]存的是g(w[i],0~sz), g1[i]存的是g1(w[i],0~sz)
//g0(n,i) = \Sigma_{j=1}^{n}[j是质数 or j的最小质因子>p[i]]*f0(j) 其中f0为f取质数时的第一个次方项
//g1(n,i) = \Sigma_{j=1}^{n}[j是质数 or j的最小质因子>p[i]]*f1(j) 其中f1为f取质数时的第二个次方项
//c0和c1等保存的是不同次方项的系数(此处只有两个系数,即假设题目中的f取质数时只有两项)
//计算f(p^t),若要降低常数也可把这个函数用增量法在调用处实现
inline LL f(LL p,LL t)
{
//...
}
//线性筛,求函数f0、f1在前i个质数处的前缀和
void init(LL n)
{
sz=0;
memset(isp,1,sizeof(isp));
isp[1]=0;
sum0[0]=0;
sum1[0]=0;
for (LL i=2; i<=n; i++)
{
if (isp[i])
{
p[++sz]=i;
//计算sum0,即sum0(i) = \Sigma_{j=1}^{i}f0(p[j])
//...
//计算sum1,即sum1(i) = \Sigma_{j=1}^{i}f1(p[j])
//...
}
for (int j=1; j<=sz&&p[j]*i<=n; j++)
{
isp[i*p[j]]=0;
if (i%p[j]==0) break;
}
}
}
inline int get_id(LL x) {
if(x<=sqrt_n) return id0[x];
else return id1[n/x];
}
//计算原理中的多项式的项, 只会计算g0(n/i), g1(n/i)
void sieve_g(LL n)
{
m=0;
for (LL i=1,j;i<=n;i=j+1)
{
LL k=n/i; j=n/k;
w[++m]=k;
if(k<=sqrt_n) id0[k]=m;
else id1[n/k]=m;
k%=MOD;
//计算原理中的g0(w[m],0),即\Sigma_{j=2}^{w[m]}f0(j), 存在g0[m]中
//...
//计算原理中的g1(w[m],0),即\Sigma_{j=2}^{w[m]}f1(j), 存在g1[m]中
//...
}
for (int i=1;i<=sz;i++)
for (int j=1;j<=m&&p[i]*p[i]<=w[j];j++)
{
int op=get_id(w[j]/p[i]);
//根据g0[j]=(g0[j]-f0(p[i])*((g0[op]-sum0[i-1]+MOD)%MOD)%MOD+MOD)%MOD计算
//...
//根据g1[j]=(g1[j]-f1(p[i])*((g1[op]-sum1[i-1]+MOD)%MOD)%MOD+MOD)%MOD计算
//...
}
}
LL S(LL x,LL y)
{
if (x<=1||p[y]>x) return 0;//base case
LL k=get_id(x),res=0;
res=((c0*g0[k]%MOD+c1*g1[k]%MOD+MOD)%MOD-(c0*sum0[y-1]%MOD+c1*sum1[y-1]%MOD+MOD)%MOD+MOD)%MOD;//质数部分的贡献
//下面的二重循环统计的是合数部分的贡献
for(int i=y;i<=sz&&p[i]*p[i]<=x;i++)//枚举合数的最小质因子
{
LL t0=p[i], t1=p[i]*p[i];
for(LL e=1; t1<=x; t0=t1,t1*=p[i],e++)//枚举最小质因子的次数
{
LL fp0=f(p[i],e), fp1=f(p[i],e+1);
(res+=(fp0*S(x/t0,i+1)%MOD+fp1)%MOD)%=MOD;
}
}
return res;
}
int main()
{
//freopen("test.in","r",stdin);
scanf("%lld",&n);
sqrt_n=sqrt(n);
init(sqrt_n); sieve_g(n);
//此处对不同次项的系数c0,c1进行直接赋值
//...
//此处计算的是原函数f在取值为1时的函数值,即f(1),存在f_1中;若是积性函数的话一般有f(1)=1
//...
printf("%lld\n",((S(n,1)+f_1)%MOD));
return 0;
}
例题
可以看到, f f f 在取质数值时有 f ( p ) = p 2 − p f(p)=p^2-p f(p)=p2−p,故可以提取出来 f 0 ( p ) = p 2 , f 1 ( p ) = p , c 0 = 1 , c 1 = − 1 f_0(p)=p^2, f_1(p)=p, c_0=1, c_1=-1 f0(p)=p2,f1(p)=p,c0=1,c1=−1,使得 f ( p ) = c 0 ∗ f 0 ( p ) + c 1 ∗ f 1 ( p ) f(p)=c_0*f_0(p)+c_1*f_1(p) f(p)=c0∗f0(p)+c1∗f1(p)。
同时, f f f 在取质数的次方时有 f ( p t ) = p t ( p t − 1 ) f(p^t)=p^t(p^t-1) f(pt)=pt(pt−1),这可以通过快速幂快速求出,或者直接在S函数中通过增量法更快地求出。
于是可以在此分析的基础上对上述模板进行代码填空:
//代码计算的是函数f(i)的前n项和
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5 + 10;//2*sqrt(n)的范围
const LL INV2 = 5e8 +4;//2的逆元(可能用到)
const LL INV6 = 166666668;//6的逆元(可能用到)
const LL MOD = 1e9 + 7;
bool isp[N];
LL n,m,sz,sqrt_n,c0,c1,p[N],w[N],id0[N],id1[N],g0[N],g1[N],sum0[N],sum1[N];
//n是输入的数, sqrt_n保存sqrt(n),即预处理的数量
//sz是质数个数, isp[i]表示i是否质数, p[i]存储第i个质数
//sum0[i]存储的是前i个质数的f0值之和,sum1[i]存储的是前i个质数的f1值之和
//m是n/k的个数, w[i]存储n/k第i种值(倒序), id0和id1[i]存储i这个值在w[i]中的下标
//g0[i]和g1[i]等分别存储f在取质数时的多项式中的不同次方项(此处只有两个数组,即假设题目中的f取质数时只有两项), g0[i]存的是g(w[i],0~sz), g1[i]存的是g1(w[i],0~sz)
//g0(n,i) = \Sigma_{j=1}^{n}[j是质数 or j的最小质因子>p[i]]*f0(j) 其中f0为f取质数时的第一个次方项
//g1(n,i) = \Sigma_{j=1}^{n}[j是质数 or j的最小质因子>p[i]]*f1(j) 其中f1为f取质数时的第二个次方项
//c0和c1等保存的是不同次方项的系数(此处只有两个系数,即假设题目中的f取质数时只有两项)
//计算f(p^t),若要降低常数也可把这个函数用增量法在调用处实现
inline LL f(LL p,LL t)
{
LL tmp=p%MOD, ret=1;
while (t>0)
{
if (t&1) (ret*=tmp)%=MOD;
(tmp*=tmp)%=MOD;
t>>=1;
}
(ret*=((ret-1+MOD)%MOD))%=MOD;
return ret;
}
//线性筛,求函数f0、f1在前i个质数处的前缀和
void init(LL n)
{
sz=0;
memset(isp,1,sizeof(isp));
isp[1]=0;
sum0[0]=0;
sum1[0]=0;
for (LL i=2; i<=n; i++)
{
if (isp[i])
{
p[++sz]=i;
//计算sum0,即sum0(i) = \Sigma_{j=1}^{i}f0(p[j])
sum0[sz]=(sum0[sz-1]+i*i%MOD)%MOD;
//计算sum1,即sum1(i) = \Sigma_{j=1}^{i}f1(p[j])
sum1[sz]=(sum1[sz-1]+i)%MOD;
}
for (int j=1; j<=sz&&p[j]*i<=n; j++)
{
isp[i*p[j]]=0;
if (i%p[j]==0) break;
}
}
}
inline int get_id(LL x) {
if(x<=sqrt_n) return id0[x];
else return id1[n/x];
}
//计算原理中的多项式的项, 只会计算g0(n/i), g1(n/i)
void sieve_g(LL n)
{
m=0;
for (LL i=1,j;i<=n;i=j+1)
{
LL k=n/i; j=n/k;
w[++m]=k;
if(k<=sqrt_n) id0[k]=m;
else id1[n/k]=m;
k%=MOD;
//计算原理中的g0(w[m],0),即\Sigma_{j=2}^{w[m]}f0(j), 存在g0[m]中
g0[m]=k*(k+1)%MOD*(2*k+1)%MOD*INV6%MOD;
g0[m]=(g0[m]-1+MOD)%MOD;
//计算原理中的g1(w[m],0),即\Sigma_{j=2}^{w[m]}f1(j), 存在g1[m]中
g1[m]=k*(k+1)/2%MOD;
g1[m]=(g1[m]-1+MOD)%MOD;
}
for (int i=1;i<=sz;i++)
for (int j=1;j<=m&&p[i]*p[i]<=w[j];j++)
{
int op=get_id(w[j]/p[i]);
//根据g0[j]=(g0[j]-f0(p[i])*((g0[op]-sum0[i-1]+MOD)%MOD)%MOD+MOD)%MOD计算
g0[j]=(g0[j]-p[i]*p[i]%MOD*((g0[op]-sum0[i-1]+MOD)%MOD)%MOD+MOD)%MOD;
//根据g1[j]=(g1[j]-f1(p[i])*((g1[op]-sum1[i-1]+MOD)%MOD)%MOD+MOD)%MOD计算
g1[j]=(g1[j]-p[i]*((g1[op]-sum1[i-1]+MOD)%MOD)%MOD+MOD)%MOD;
}
}
LL S(LL x,LL y)
{
if (x<=1||p[y]>x) return 0;//base case
LL k=get_id(x),res=0;
res=((c0*g0[k]%MOD+c1*g1[k]%MOD+MOD)%MOD-(c0*sum0[y-1]%MOD+c1*sum1[y-1]%MOD+MOD)%MOD+MOD)%MOD;//质数部分的贡献
//下面的二重循环统计的是合数部分的贡献
for(int i=y;i<=sz&&p[i]*p[i]<=x;i++)//枚举合数的最小质因子
{
LL t0=p[i], t1=p[i]*p[i];
for(LL e=1; t1<=x; t0=t1,t1*=p[i],e++)//枚举最小质因子的次数
{
LL fp0=f(p[i],e), fp1=f(p[i],e+1);
(res+=(fp0*S(x/t0,i+1)%MOD+fp1)%MOD)%=MOD;
}
}
return res;
}
int main()
{
//freopen("test.in","r",stdin);
scanf("%lld",&n);
sqrt_n=sqrt(n);
init(sqrt_n); sieve_g(n);
//此处对不同次项的系数c0,c1进行直接赋值
c0=1, c1=-1;
//此处计算的是原函数f在取值为1时的函数值,即f(1),存在f_1中;若是积性函数的话一般有f(1)=1
LL f_1=1;
printf("%lld\n",((S(n,1)+f_1)%MOD));
return 0;
}

本文介绍了Min-25筛的原理、适用条件及其在积性函数求和中的应用。通过相关链接提供了学习资源,并强调Min-25筛在满足特定条件的函数中能实现低于线性的复杂度。文章还包含模板代码示例及解题思路,例如用于求质数前缀和的问题。
2679





