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\),定义如下:

\[S(n,j)=\sum\limits_{i=1}^{n}f(j)[\min\limits_{p\mid i} p>P_j] \]

然后对这个函数分类讨论:

(1).\(i\) 为质数

我们记函数 \(q\)

\[q(n)=\sum\limits_{i=1}^{n}f(i)[i\in\mathbb{P}] \]

即为 \(n\) 以内所有素数的 \(f\) 之和。

此时通过容斥,\(f(n,j)\)\(i\) 为质数处的贡献之和为:

\[q(n)-\sum\limits_{i=1}^{j}f(P_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\) 以内的质数,可以通过线性筛筛出来。

接下来写出贡献之和:

\[\sum\limits_{k>j}^{P_k^2\le n} \sum\limits_{e=1}^{P_k^e\le n} f(P_k^e) \times \Big( S(\frac{n}{P_k^e},k) + [e>1] \Big) \]

我们解释一下求和里头为什么长这个样子:

注意,我们枚举的这个合数,它里面的 \(P_k\) 一共只有 \(e\)。我们记这个合数为 \(P_k^e\times t\),就一定有 \(\gcd(P_k^e,t)=1\)

我们可以记 \(t\) 的最小质因子为 \(P_T\)

注意到三点:

  1. 由于 \(f\) 为积性函数,且 \(\gcd(P_k^e,t)=1\) ,我们就可以考虑把 \(f(P_k^e)\) 提出来,变成 \(f(P_k^e) \times f(t)\)

  2. 这个合数的最大值显然为 \(n\),而因为 \(P_k^e\times t\),我们发现 \(t\) 的上界为 \(\dfrac{n}{P_k^e}\)

  3. 由于 \(P_k\) 为这个合数的最小质因子,\(t\) 还和 \(P_k^e\) 互质,那么 \(t\)最小质因子一定比 \(P_k\),即 \(P_T>P_k\),即 \(T>k\)

通过这两点,我们差不多可以明白

\[f(P_k^e) \times S(\frac{n}{P_k^e},k)\]

是怎么个事了,后者的实际含义就是在求和 \(f(t)\) 的贡献

两个参数中,前者在表达 \(t\) 的上界为 \(\dfrac{n}{P_k^e}\) ,后者在表达 \(t\)最小质因子一定比 \(P_k\) 大,即 \(T>k\),也就是我们注意到的第二点和第三点。

这一部分就解释清楚了!

但还有一个东西:

\[f(P_k^e)[e>1] \]

是啥东西?

其实就是在表达当 \(e>1\) 时,\(f(P_k^e)\) 本身的贡献也要算进来。

为什么必须是 \(e>1\)?因为当 \(e=1\) 时,\(f(P_k)\) 就成了质数,在上一个质数部分算过贡献了。


于是我们把 \(i\) 为质数和 \(i\) 为合数的两部分贡献式合并一下,就得到了 \(S\) 的转移式:

\[S(n,j)=q(n)-\sum\limits_{i=1}^{j}f(P_i) + \sum\limits_{k>j}^{P_k^2\le n} \sum\limits_{e=1}^{P_k^e\le n} f(P_k^e) \times \Big( S(\frac{n}{P_k^e},k) + [e>1] \Big) \]

可以发现,我们这个题要求的前缀和就是 \(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\) 里头的变量,只有:

\[\lfloor\frac{n}{1}\rfloor,\lfloor\frac{n}{2}\rfloor\dots \lfloor\frac{n}{n}\rfloor \]

这些可能的取值,这是因为:

\[\lfloor\frac{\lfloor\frac{a}{b}\rfloor}{c}\rfloor=\lfloor\frac{a}{bc}\rfloor \]

写了那么多遍数论分块,我们肯定知道这个东西的取值一共有 \(2\sqrt n\) 种。

于是我们只需要处理处 \(q\) 取这 \(2\sqrt n\) 种取值下的值,就能够对 \(S\) 进行转移了!

Part 2

\(q(n)\) 函数是个啥?就是对于 \(n\) 以内所有的质数,它们的 \(f\) 之和。

虽然说 \(f\) 是个积性函数,但它其实是个项数很少的多项式的形式。

假如说 \(f\) 长这样:

\[f(x)=a_0x+a_1x^1+\dots a_tx^t \]

我们就可以给它拆成 \(t+1\)完全积性函数,为:

\[\begin{aligned}&f'_0(x)=a_0x\\&f'_1(x)=a_1x^1\\&\dots \\&f'_t(x)=a_tx^t\end{aligned} \]

进而有 \(f(x)=\sum\limits_{i=1}^{t}f'_i(x)\)

我们可以分别求出每一个 \(f'\),在 \(n\) 以内所有质数处的取值之和,然后对它们求个和,就能够得到 \(q(n)\) 了!

一定要记住,我们只关注质数处的取值,所以合数处的答案变没变不重要

至于为什么我们非要把 \(f\) 拆开成 \(f'\),因为 \(f'\)完全积性函数,接下来的推导需要完全积性函数的性质。


我们针对某一个 \(f'\) 进行讨论:

我们设一个函数 \(g\)

\[g(n,j)=\sum\limits_{i=2}^{n}f'(i)[i\in\mathbb{P}\mid \min\limits_{p|i}p>P_j] \]

这里面的符号 \(\mid\) 为或。

这个函数的含义就是:\(n\) 以内所有的质数的 \(f'\) 之和,再加上一部分最小质因子大于 \(P_j\) 的合数的 \(f'\) 之和。

我们注意到过,如果确定了一个合数的最小质因子 \(P_k\),那么这个合数最小为 \(P_k^2\)

于是我们可以通过取一个合适的 \(j\),使得 \(g(n,j)\) 只包含质数的贡献。

我们可以用 \(g\) 把这个 \(f'\)\(q\) 的贡献表示出来,设为 \(q'(n)\),即:

\[q'(n)=g(n,\max\limits_{P_k^2\le n}k) \]

所以我们想办法把 \(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)\) 提出来。

注意到:

  1. \(x\) 的上界是 \(n\),所以 \(t\) 的上界为 \(\dfrac{n}{P_j}\)

  2. \(x\) 的最小质因子为 \(P_j\),所以 \(t\) 的最小质因子一定大于等于 \(P_j\),这是下界。

因此我们可以写出多出来的这部分贡献的模样:

\[f'(P_j)\times \Big( g(\frac{n}{P_j},j-1) - \sum\limits_{i=1}^{j-1}f'(P_i)) \Big) \]

里面 \(g\) 就是在枚举 \(t\),而由于 \(P_i\)\(x\) 的最小质因子,所以 \(t\) 的枚举中不能有小于 \(P_i\) 的质数提供贡献,于是我们减去了 \(\sum\limits_{i=1}^{j-1}f'(P_i)\)


综合以上两部分,我们就能把 \(g\) 的总递推式写出来了:

\[g(n,j)=\begin{cases}g(n,j-1) & P_j^2 > n \\ g(n,j-1) - f'(P_j)\times \Big( g(\frac{n}{P_j},j-1) - \sum\limits_{i=1}^{j-1}f'(P_i)) \Big) & P_j^2\le n\end{cases}\]

我们用滚动数组滚掉 \(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
posted @ 2024-02-19 15:13  一匹大宝羊  阅读(18)  评论(0编辑  收藏  举报