筛法入门
神秘算法。
符号与约定
约定 $p$ 表示素数,$\prod_i p_i^{c_i}$ 表示某个数的质因数分解。
$f\ast g$ 表示 $f$ 与 $g$ 的狄利克雷卷积。大写字母 $F(i)$ 表示对应小写字母 $f(i)$ 的前缀和,即 $F(n)=\sum_{i=1}^nf(i)$(同理,$G(i),g(i)$ 等也有类似关系)。
$[]$ 为艾佛森括号,表示若内部表达式为真,则值为 $1$,否则为 $0$。
线性筛
大部分人最早接触的筛法,最初级的用法是用来筛质数。
事实上一般还是线性筛用的最多。
质数
考虑一个合数含有至少两个质因子,那么从小到大枚举质数,枚举它的所有倍数打一个合数标记。后面每次遇到没标记的就是质数进行,否则就是合数。这个是埃式筛,根据神秘证明复杂度是 $O(n\log \log n)$。
如果每个合数只被标记一次,复杂度就能缩到 $O(n)$ 了。一个想法是让每个合数只被它的最小质因子标记一次。那么先枚举 $i$,再枚举之前找到的质数 $p$,给 $ip$ 打标记。如果第一次遇到 $i\bmod p=0$ 说明现在 $p$ 就是 $i$ 的最小质因子,如果再往后枚举 $p$,那么 $ip$ 就不是被最小质因子(即 $i$ 的最小质因子)标记的。所以遇到 $i\bmod p=0$ 就退出循环即可。
求积性函数的值
这玩意只用来筛素数也太浪费了。容易发现每次 $i,p$ 要么互质,要么 $p$ 是 $i$ 的最小质因子。那么对于一个积性函数 $f(i)$,前一种情况有 $f(ip)=f(i)f(p)$,后一种情况有 $f(ip)=f(\frac{i}{p^c})f(p^{c+1})$,其中 $c$ 是 $i$ 中 $p$ 的指数。
多数情况下如果要计算积性函数一般 $f(p^c)$ 是好求的。于是可以记录每个数最小质因子的指数以及最小质因子以外部分的 $f$ 值,就能比较方便地求任意积性函数了。复杂度 $O(n)$,因为每次访问下一个数是 $O(1)$ 的,每个数至多访问一遍。
求欧拉函数/莫比乌斯函数的值
这两个也是积性函数,但是拎出来单独说一下。
欧拉函数比较特殊,根据通项公式 $\varphi(n)=\prod_{i}p_i^{c_i-1}(p_i-1)$ 有 $\varphi(p^c)=p\cdot \varphi(p^{c-1})$,于是在 $i\bmod p=0$ 时可以直接计算。
莫比乌斯函数就更特殊了,因为在某质因子质数大于 $1$ 时值取 $0$,所以遇到 $i\bmod p=0$ 时 $\mu(ip)=0$。
杜教筛
难度陡然上升。
为什么杜教筛叫筛呢?(我个人理解)注意到线性筛是通过更小的多个 $f(n)$ 用较小的复杂度计算出更大的单个 $f(n)$,虽然真正计算过的函数值变多了,但是你真正算出 $f(n)$ 所用的时间变少了。杜教筛也是如此,通过多计算几个值的方式降低整体复杂度。
主要思路
考虑一对数论函数 $f,g$,有如下关系:
$$ \sum_{i=1}^n(f\ast g)(i)=\sum_{i=1}^n\sum_{xy=i}g(x)f(y)=\sum_{i=1}^ng(i)F(\left\lfloor\frac{n}{i}\right\rfloor) $$
因为每一对 $xy\le n$ 都会产生 $g(x)f(y)$ 的贡献,假如把前缀和拆开就会发现上述式子成立。
于是有:
$$ \begin{aligned} \sum_{i=1}^n(f\ast g)(i)&=\sum_{i=1}^ng(i)F(\left\lfloor\frac{n}{i}\right\rfloor)\\ \sum_{i=1}^n(f\ast g)(i)-\sum_{i=2}^{n}g(i)F(\left\lfloor\frac{n}{i}\right\rfloor)&=g(1)F(n)\\ \frac{\sum_{i=1}^n(f*g)(i)-\sum_{i=2}^{n}g(i)F(\left\lfloor\frac{n}{i}\right\rfloor)}{g(1)}&=F(n)\\ \end{aligned} $$
假如 $f\ast g$ 和 $g$ 的前缀和都好算(可以 $O(1)$ 或者低复杂度算),那么对前面这一堆数论分块,就能转化成 $O(\sqrt{n})$ 个子问题。
于是得到了杜教筛的基本思路。构造 $g$ 使得 $f\ast g$ 和 $g$ 的前缀和都好求。
注意到 $\left\lfloor\frac{\left\lfloor\frac{a}{n}\right\rfloor}{m}\right\rfloor=\left\lfloor\frac{a}{nm}\right\rfloor$,所以需要计算的 $F(n')$ 都是 $\left\lfloor\frac{n}{i}\right\rfloor$ 中出现过的。那么全部取出来从小往大计算即可(或者记忆化)。
复杂度证不来,就是 $\left\lfloor\frac{n}{i}\right\rfloor$ 的每个取值开根号加起来,可以证明是 $O(n^{\frac{3}{4}})$ 的。
一种优化是(在能做的情况下)直接预处理 $F(i)$ 到 $n^{\frac{2}{3}}$(一般来说 $f$ 是积性函数,可以线性筛),然后对于小的直接引用,大的做上面的方法,可以证明复杂度是 $O(n^{\frac{2}{3}})$ 的。
莫比乌斯函数
最常用的一集。
有经典式子 $\varepsilon=\mu\ast I$。而 $\varepsilon$ 和 $I$ 的前缀和都巨好求无比($\sum_{i=1}^n\varepsilon(i)=1,\sum_{i=1}^nI(i)=n$)。于是可以直接算。
欧拉函数
怎么哪都是你们两个。
有经典式子 $\varphi=id\ast \mu$。故 $id=\varphi \ast I$,于是直接套板子。
其他函数
很随缘啊,取决于能不能构造出 $g$。
但是不要局限于筛。假如你能构造出 $g,h$ 满足 $f=g\ast h$,而 $g,h$ 都好求前缀和,其实可以直接用 $\sum_{i=1}^n(f\ast g)(i)=\sum_{i=1}^ng(i)F(\left\lfloor\frac{n}{i}\right\rfloor)$ 直接数论分块算,复杂度还是 $O(n^{\frac{1}{2}})$ 的。
模板题参考代码
#include<bits/stdc++.h>
#define forup(i,s,e) for(i64 i=(s),E123123123=(e);i<=E123123123;++i)
#define fordown(i,s,e) for(i64 i=(s),E123123123=(e);i>=E123123123;--i)
#define mem(a,b) memset(a,b,sizeof(a))
#ifdef DEBUG
#define msg(args...) fprintf(stderr,args)
#else
#define msg(...) void();
#endif
using namespace std;
using i64=long long;
using pii=pair<i64,i64>;
#define fi first
#define se second
#define mkp make_pair
#define gc getchar()
i64 read(){
i64 x=0,f=1;char c;
while(!isdigit(c=gc)) if(c=='-') f=-1;
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc;}
return x*f;
}
#undef gc
const i64 N=1e7+5,inf=0x3f3f3f3f;
i64 n,mu[N],summu[N],phi[N],sumphi[N];
vector<i64> pri;
void init(i64 n){
mu[1]=1;phi[1]=1;
forup(i,2,n){
if(!phi[i]){
phi[i]=i-1;mu[i]=-1;
pri.push_back(i);
}
for(auto j:pri){
if(1ll*i*j>n) break;
if(i%j){
phi[i*j]=phi[i]*(j-1);
mu[i*j]=-mu[i];
}else{
phi[i*j]=phi[i]*j;
mu[i*j]=0;
break;
}
}
}
forup(i,1,n){
summu[i]=summu[i-1]+mu[i];
sumphi[i]=sumphi[i-1]+phi[i];
}
}
map<i64,pii> sum;
pii calc(i64 n){
if(n<=1e7){
return mkp(summu[n],sumphi[n]);
}
if(sum.count(n)) return sum[n];
i64 smu=1,sphi=1ll*(1+n)*n/2;
for(i64 l=2,r=0;l<=n;l=r+1){
r=n/(n/l);
pii res=calc(n/l);
smu-=(r-l+1)*res.fi;
sphi-=(r-l+1)*res.se;
}
smu/=1;sphi/=1;//象征性除以 1,防止复制板子的时候忘了。
sum[n]=mkp(smu,sphi);
return mkp(smu,sphi);
}
void solve(){
n=read();
pii res=calc(n);
printf("%lld %lld\n",res.se,res.fi);
}
signed main(){
init(1e7);
i64 t=read();
while(t--){
solve();
}
}
Powerful Number 筛(PN 筛)
PN 筛用于亚线性地求解一些积性函数的前缀和。
Powerful Number
Poweful Number,Number 中的 Number,数字中的数字,数字中的征服者,数字的王
若一个正整数 $n=\prod p_i^{c_i}$,$\forall i$ 均有 $c_i>1$,则称 $n$ 为一个 Powerful Number(注意 $1$ 也满足这个条件)。
显然所有 PowerFul Number 都能写成 $a^2b^3$,考虑偶数就全放 $a$ 里,奇数就拆三个放 $b$ 里。
那么 $n$ 以内的 Powerful Number1 个数就是 $\sum_{a=1}^{\sqrt{n}}\sqrt[3]{\frac{n}{a^2}}$。根据一些我不会的证明这玩意是 $O(\sqrt{n})$ 级别的。
PN 筛
首先我们需要构造一个好求前缀和的积性函数 $g$,满足对于所有质数 $p\le n$ 均有 $g(p)=f(p)$(非质数点位不作要求,比如若 $f$ 在质数位置都取 $1$ 时,$g$ 可能可以取 $I$)。
考虑构造一个 $h\ast g=f$。根据狄利克雷卷积的性质,有 $h(1)=1$ 且 $h$ 也是积性函数。下面探讨一些 $h$ 的性质。
对于任意质数 $p$,显然有 $f(p)=h(p)g(1)+h(1)g(p)$。由于 $g(1)=h(1)=1$(非 $0$ 积性函数的性质),则 $f(p)=h(p)+g(p)$。又由于 $g(p)=f(p)$,所以 $h(p)=0$。而注意到 $h$ 为积性函数,容易发现在非 Powerful Number 的位置均有 $h(i)=0$(否则就会带一个一次质因子,根据积性 $h(i)=0$)。
关于 $h$ 的构造,因为它是积性函数,所以其实也只需要考虑 $p^k$ 就行了。能求出 $h$ 的通项公式当然好,但如果求不出来也有办法。考虑 $f(p^k)=\sum_{i=0}^kh(p^i)g(p^{k-i})$,所以 $h(p^k)=f(p^k)-\sum_{i=0}^{k-1}h(p^i)g(p^{k-i})$。又因为 $O(\sqrt{n})$ 内指数数量约为 $O(\frac{\sqrt{n}}{\log n})$ 级别的,素数的幂次上界是 $\log n$ 的。所以直接暴力复杂度是 $O(\frac{\sqrt{n}}{\log n}\log^2n)=O(\sqrt{n}\log n)$,还是非常小的,而且这个上界显然非常松吧。另外空间复杂度是 $O(\frac{\sqrt{n}}{\log n}\log n)=O(\sqrt{n})$ 的。当然如果能求出 $h$ 的通项公式那么复杂度会低一点。
由于 $F(n)=\sum_{i=1}^n(h\ast g)(i)=\sum_{i=1}^nh(i)G(\left\lfloor\frac{n}{i}\right\rfloor)$,而 $h$ 只在 PN 处有值。则大力搜索 $i$(枚举 $\sqrt{n}$ 以内的质数再从 $2$ 开始枚举其幂次)然后乘上 $g$ 的前缀和即可。复杂度 $O((p+q)\sqrt{n})$,其中 $p,q$ 分别是求 $g$ 前缀和的复杂度和求 $h$ 单点值的复杂度。
结语
感觉学了杜教筛和 PN 筛就差不多了啊。
这篇博客提到的筛法的核心是构造 $g,h$ 函数(除了线性筛),这个就和做几何题一样,想到了就简单,没想到就暴毙,感觉很考验经验啊。