【学习笔记】数论之筛法

前言:

可以会乱记一些技巧吧。

交换求和顺序

如果不确定可以将条件写成 [A] 的形式,交换完求和顺序再把这个条件放里面。
例如:

i=1nd[d|i]=d=1ni[d|i]=d=1ni=1nd1

狄利克雷前缀和与狄利克雷差分

设两个数论函数满足 fI=g
狄利克雷前缀和是指给定 fg
狄利克雷差分就是倒过来,也就是给定 gf,乘 μ 就好。

狄利克雷前缀和就是知道恰好为 x 的信息,要求为 x 的倍数的信息。
狄利克雷差分就是知道为 x 的倍数的信息,要求恰好为 x 的信息。

埃氏筛:

基础知识

埃氏筛的代码实现如下:

点击查看代码
void pre_work(int mx){
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			prime[++tot] = i;
		}
		for(int j=1; j<=tot && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0)	break;
		}
	}
}

[1,n] 中的质数个数为 nlogn,所以第 i 个质数为 O(ilogi)
所以埃氏筛的时间复杂度为 O(nloglogn)

线性筛:

基础知识

线性筛的代码实现如下:

点击查看代码
void prime()
{
	for(int i=2; i<=n; i++)
	{
		if(!v[i]) p[++cnt]=i;
		for(int j=1; j<=cnt && i*p[j]<=n; j++)
		{
			v[i * p[j]] = 1;
			if(i % p[j] == 0) break;	
		}
	}
}

线性筛中内层循环相当于枚举最小质因子,如果满足 imodprimej=0 也就是意味着此时 i×primekk>j 的最小质因子不是 primek,而至少应为 primej 所以可以直接跳过

所以每个数只会被其最小质因子筛一次,复杂度 O(n)

常见积性函数筛法

线性筛的强大作用之一就是筛积性函数。

idk(i)

  • 可以对于 idk(p)(pprime) 快速幂求,然后线性筛的过程中直接乘就是答案(注意到 idk 是一个完全积性函数)

φ(i)

  • imodp0,则 φ(i×p)=φ(i)×(p1)
  • imodp=0,则 φ(i×p)=φ(i)×p

μ(i)

  • imodp0μ(i×p)=μ(i)
  • imodp=0μ(i×p)=0

一般积性函数筛法

若对于积性函数 f 可以以 O(n) 的复杂度求出所有的 f(pc)(pprime),那么就可以很快求解。

根据线性筛我们可以求出 mni 表示数 i 的最小质因子,求 pwi 代表对 i 质因数分解之后 mni 对应的系数,则可以直接得到:f(i)=f(pwi)f(ipwi)

就以下面这个为例推一推:

f(x)=xμ(x),要求筛 g=fIid2

积性函数卷积性函数还是积性函数,所以 g 是积性函数。

因为 μI=e,所以考虑先计算 fI

(fI)(pc)=i=0cpiμ(pi)={1c=0(1p)c>0

注意因为我们只在意 pc 所以枚举因数就相当于枚举 p 的指数。
所以:

g(pc)=i=0c(fi)(pi)×id2(pci)=1×p2c+i=1c(1p)p2(ci)=i=0cp2ipi=0c1p2i=(1p2(c+1))p(1p2c)1p2

所以知道了这个就很简单了。

杜教筛

基础知识

杜教筛的作用就是求数论函数 f 的前缀和。

杜教筛首先需要我们构造一个数论函数 g,满足 fg=h,通常情况下使用杜教筛的条件是 g,h 的前缀和很好求。

为了方便下面设 S(f,n) 表示数论函数 f 的前 n 项和。

S(h,n)=i=1nd|ig(d)f(id)=d=1ng(d)i=1ndf(i)=d=1ng(d)S(f,nd)

因为我们要求的是 S(f,n) 所以就考虑把这一项单独提出来:

g(1)S(f,n)=S(h,n)d=2ng(d)S(f,nd)S(f,n)=S(h,n)d=2ng(d)S(f,nd)g(1)

对于后面显然需要整除分块求解,所以必须要快速求 d=lrg(d)
经过复杂度分析可以知道,当我们线性预处理出 S(f,n) 的前 O(n23) 项,我们的总时间复杂度就是 O(n23)

下面就是一个模板代码实现:

点击查看代码
inline ll F (register ll n) {
	if (n <= 3e6) return sumf[n]; // 预处理出 n 较小时的前缀和
	if (f[n]) return f[n]; // 记忆化,如果求过这个值,就不需要再递归一遍了
	register ll ans = sum (f * g); // 这是 f * g 的 n 项前缀和
	for (register ll l = 2, r; l <= n; l = r + 1) // 整除分块
		r = n / (n / l), ans -= (sumg[r] - sumg[l - 1]) * F (n / l); 
		// [l,r] 的 F (n / l) 是一样的,对 g(x) 求个和即可
	return f[n] = ans / g[1]; // 别忘了除上 g(1)
}

简单应用:

如果 fg=h 中两个可以杜教筛一个可以线性筛,则三个都可以杜教筛。

数论函数点乘:fg(fg)(n)=f(n)g(n)
若有完全积性函数 c 有:(ac)(bc)(ab)c

例题一:对 f=μ 做杜教筛
解法:取 g=I,则 fg=e

例题二:对 f=φ 做杜教筛
解法:取 g=I,则 fg=id

例题三:对 f=idkφ 做杜教筛(注意这里是点乘)
解法:取 g=idk,则 fg=(φidk)(Iidk)=(φI)idk=ididk=idk+1

例题四:对 f=idk·μ 做杜教筛(注意这里是点乘)
解法:取 g=idk,则 fg=eidk=e

例题五:对 f=μ2(μid) 做杜教筛
解法:取 g=id,则 fg=μ2((μI)id)=μ2e=μ2μ2(i)=d2|iμ(d),所以其前缀和也是好求的

有了上面这些知识你就可以通过 P4213 【模板】杜教筛(Sum) 了。
代码如下:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
const int MX = 5000000;
int tot,prime[N],phi[N],mu[N];
bool flag[N];
unordered_map<int,int> pre_mu,pre_phi;
void pre_work(int mx){
	mu[1] = 1;phi[1] = 1;
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			mu[i] = -1,phi[i] = i-1;
			prime[++tot] = i;
		}
		for(int j=1; j<=tot && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0){
				phi[i * prime[j]] = phi[i] * prime[j];
				break;
			}
			else	phi[i * prime[j]] = phi[i] * (prime[j] - 1),mu[i * prime[j]] = -mu[i];
		}
	}
	for(int i=1; i<=mx; i++)	mu[i] = mu[i-1] + mu[i];
	for(int i=1; i<=mx; i++)	phi[i] = phi[i-1] + phi[i];
}
int get_mu(int n){
	if(n <= MX && mu[n])	return mu[n];
	if(pre_mu.count(n))	return pre_mu[n];
	int tmp = 1;
	for(int l=2,r; l<=n; l = r + 1){
		r = n / (n / l);
		tmp -= (r - l + 1) * get_mu(n / l);
	}
	tmp /= 1;
	return pre_mu[n] = tmp;
}
int get_phi(int n){
	if(n <= MX && phi[n])	return phi[n];
	if(pre_phi.count(n))	return pre_phi[n];
	int tmp = n * (n + 1) / 2;
	for(int l=2,r; l<=n; l = r + 1){
		r = n / (n / l);
		tmp -= (r - l + 1) * get_phi(n / l);
	}
	tmp /= 1;
	return pre_phi[n] = tmp; 
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	pre_work(MX);
	int t;
	scanf("%lld",&t);
	while(t--){
		int n;
		scanf("%lld",&n);
		printf("%lld %lld\n",get_phi(n),get_mu(n));
	}
	return 0;
}

贝尔级数

基础知识:

如果你看了上文中线性筛-一般积性函数筛法那一道例题,你就会惊奇的发现,当我们只关系 f(pc) 的值时,狄利克雷卷积就是相当于加法卷积。于是就有贝尔级数:

fp(x)=i=0f(pi)xi

如果你对生成函数有所了解会发现这东西就跟 OGF 一样,但是你如果没学也不要紧,下文会将你当作没学来看待。

因为相当于加法卷积,所以也就有:

fg=hp,fp(x)gp(x)=hp(x)

(第一个 代表狄利克雷卷积,第二个 代表加法卷积)
下面就列几个贝尔级数:

ep(x)=1Ip(x)=i=0xi=11x(idk)p(x)=i=0pikxi=11pkxup(x)=(I1)p(x)=1xup2(x)=1+xdp(x)=i=0(i+1)xi=1(1x)2(σk)p(x)=(Iidk)p(x)=1(1x)(1pkx)φp(x)=(μid)p(x)=1x1px

对于 up(x),up2(x) 也可以直接枚举只有前两项有值,也可以得到同样的结论

对于 dp(x) 可以考虑组合意义,也就是两个物品每个物品可以选无限多次,询问选 i 个的方案就是 i+1,或者可以理解为 dp(x)=Ip(x)Ip(x)

关于 idk 在点乘中对贝尔级数的影响有如下结论:

(fidk)p(x)=fp(pkx)

也就是说可以直接将 pkx 视为一个整体。
证明的话就是暴力展开:

(fidk)p(x)=i=0f(pi)pikxi=fp(pkx)

上述结论的简单应用就是:

  • μidk=1pkx
  • φidk=1pkx1pk+1x

简单应用:

例题一:对 f(pk)=pk+[k>0](1)k 且已知 f 为积性函数,做杜教筛
解法:
考虑先求出 f 的贝尔级数:

fp(x)=1+i=1(pi+(1)i)xi=1+11px+11+x

考虑构造卷积 gp(x)=(1px)(1+x),则 (fg)p(x)=1+px2
分别考虑这两个的前缀和是不是很好做。
先考虑 g(1px)=μ·id,(1+x)=μ2,所以 g=μ2(μid),这个就是我们杜教筛的例题五。
hp(x)=1+px2,以实际意义就是 h(n)0n 都满足质因子指数为 2,所以就直接枚举就好了:

S(h,n)=i=1niμ2(i)

上面的式子其实是将合法的数直接开方做的一个映射。

Powerful Number

基础知识:

对于正整数 n,设其质因数分解的结果为 n=ipiai,满足 i,ai>1 则称 n 为一个 Powerful Number,其实就是满足其所有质因子的指数大于等于 2
其有如下的两个性质:

  • 任意一个 Powerful Number 都可以表示为 a2b3 的形式
  • 小于等于 n 的 Powerful Number 只有 O(n)

证明显然。

我们就有 PN 筛就是使用了 Powerful Number 的性质。

PN 筛也是求解一个积性函数的前缀和。

首先要构造一个易求出前缀和的积性函数 g,满足对于所有的质数 p 都有 g(p)=f(p)

h=f/g,也就是 f=gh,易得 h 也是一个积性函数,即 h(1)=1

考虑对于一个质数 pf(p)=g(1)h(p)+g(p)h(1)=h(p)+g(p),因为 f(p)=g(p) 所以 h(p)=0,也就是 h 在所有的质数位置的值 0,因为 h 也是一个积性函数,易得 h 只有在 Powerful Number 位置值不为 0

根据 f=gh 推式子:

S(f,n)=i=1nf(i)=i=1nd|ih(d)g(id)=d=1nh(d)i=1ndg(i)=d=1nh(d)S(g,nd)

可以直接 O(n) 找出所有的 Powerful Number 然后算出所有 h(pc) 处的值就可以根据积性函数求出 h 的所有有效值,对于所有的有效值对答案累加上 h(d)S(g,nd) 即可。

下面第一个就是究竟该怎么样才能找到 Powerful Number,就是直接枚举 pc(c>1,pn),然后搜索即可。

然后就是如何求出 h(pc) 位置的值,因为 f=gh,所以 f(pc)=d=0ch(pd)g(pcd),移项得:h(pc)=f(pc)d=1ch(pd)g(pcd)

简单应用:

例题一:P5325 【模板】Min_25筛
解法:考虑 f(x)=x(x1) 构造 g(x)=xφ(x),然后剩下的就按照上文的套路推就好了。

Min_25 筛

基础知识

Min_25 筛可以做到以亚线性得复杂度求解积性函数的前缀和。
其要求为:f(p) 为低阶多项式,f(pc) 可以快速计算。
其基本思想就是用埃氏筛的想法,将问题拆分成与质因子相关的子问题。

为了下文推导方便,我们有如下规定:
primei 表示第 i 个质数的值
|primen| 表示 [1,n] 中质数的个数
mni 代表 i 的最小质因子

我们要求的就是 f 的前缀和,考虑构造 g(n,i) 表示在埃氏筛中小于等于 n 的数里,前 k 个质数筛完后剩下数的 f 之和。也就是所有的质数的 f 和所有最小质因子大于 primek 的合数的 f 之和。

其实也就是说:

g(n,i)=jprimemnj>if(j)

可以显然发现 g(n,|primen|) 代表 [1,n] 所有质数的 f 之和,g(primek,k) 代表前 k 个质数的 f 之和。

会发现 g(n,i) 满足如下的递推式:

g(n,i)={g(n,i1)primei2>ng(n,i1)f(primei)×(g(nprimei,i1)g(primei1,i1))primei2n

第一个转移很好理解,因为此时 primei 不会筛掉任何一个数

第二个转移就有点神仙了,考虑 mnx=primei 相当于将数除去 primei 后,其最小质因子大于 primei1,其实就是对应着 g(nprimei,i1),可是此时我们也会将 j=1i1f(primej) 的贡献删掉,但是根据定义我们不应该删掉,所以最后就用 g(primei1,i1) 将这一部分贡献补回来。

但是我们会发现能把 f 提出来的条件就是 f 是一个完全积性函数,但是它不是,那么该怎么办呢。

所以我们就想办法构造一个完全积性函数啦,因为我们的 f 一般都是多项式的形式,而幂是一个完全积性函数,所以可以考虑对于 f 的每一项分别通过 g 递推然后加和即可。

考虑我们需要对于 p[1,n] 都求出 g(p,i) 嘛,其实没有必要,因为我们递归下去的形式为 nx,这其实就是数论分块的形式,所以其实只用 O(n) 种不同的位置需要求,然后在这个上面滚动就好了。

现在处理完了 g(n,i) 考虑一个新的式子,设 S(n,i) 表示小于等于 n 的数里,最小质因子大于 primei 的数的 f 值的和。

那么我们的答案就可以表示为:S(n,0)+f(1)

那么 S(n,i) 就满足如下的式子:

S(n,i)=g(n,|prime|)g(|primei|,|prime|)+j>ipjknf(pjk)S(npjk,j+[k>1])

其实就是分为两部分计算贡献。

第一部分为:大于 primei 的质数
这一部分的贡献就直接通过得到的 g 就可以快速取得

第二部分为:最小质因子大于 primei 的质数,可以考虑直接枚举最小质因子以及其指数,然后就可以直接递归求解。

根据某个神仙的定理,即使我们在递归过程中不记忆化复杂度依旧正确。

posted @   linyihdfj  阅读(106)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示