筛法的引入
首先我们给出一个问题:
求1~N以内的所有质数
最朴素的算法是一一枚举
1~N以内的数,验证是否为素数
一般来说我们验证素数的代价是O(N−−√)(当然你也可以用赌徒算法),这样我们解决这个问题就需要O(NN−−√)的代价
那么我们能否做的更好呢?
我们需要的就是筛法
朴素的筛法: Eratosthenes 筛法
由唯一分解定理我们知道,一个合数总是能分解为若干个质数的乘积,因此我们产生这样一个想法:把所有是质数倍数的数都去掉(假如一开始我只知道2是质数),那么剩下的就是质数了。
基于这一想法,我们创造了
Eratosthenes
筛法
代码如下:
memset(check, false, sizeof(check))
int tot = 0;
for (int i = 2; i < N; i++)
if (! check[i]){
prime[tot ++] = i;
for (int j = i * 2; j <= N; j++) check[j] = true;
}
那么这么做的时间复杂度如何呢?
显然复杂度
T(n)=∑p≤nnp=n∑p≤n1p
那么问题就在于
n以内的质数的倒数和是多少
显然这个比调和级数小(即所有数的倒数和),调和级数又比
ln小(由积分可以知道)
,所以这至多是一个
O(nlnn)
的算法
具体更为精细的分析,
Euler
在他的论文里就指出
∑p≤n1p=lnlnn
因此
Eratosthenes筛法
的复杂度是
O(nlnlnn)
注:同样我们可以发现一个数
n不断地开根号取整最多开loglogn次
,我没有发现两者之间的联系,如果有人发现的话请务必告诉我
线性的筛法: Euler 筛法
Euler筛法
基于这样一个想法:每个数只被筛去一次,如果我能做到这一点,那么显然这样的筛法就是
O(n)的
具体做法我们先看代码:
memset(check, false, sizeof(check));
int tot = 0;
for (int i = 2; i <= N; i++){
if (!check[i]) prime[tot++] = i;
for (int j = 0; j < tot; j++){
if (i * prime[j] > N) break;
check[i * prime[j]] = true;
if (i % prime[j] == 0) break;
}
}
Euler筛每次都把当前的i与素数表prime里的素数pj相乘,我们设i的最小质因子是p′,那么在pj<p′时,pj都是pj∗i这个数的最小质因子,我们将它筛去,一旦发现pj=p′,也就是pj|i的时候,就意味着如果pj再变大,p′就将成为pj∗i的最小质因子,之后的所有数已经被p′筛过一次了,此时我们就可以跳出循环了
综上, Euler筛的复杂度为O(n)
Euler 筛法的应用:求积性函数
定义:
定义在正整数集上的函数f,对于任意两个互质的整数a,b如果有f(ab)=f(a)∗f(b),那么f是积性函数
常见积性函数:
欧拉函数
ϕ
,莫比乌斯函数
μ
线性求解:
求积性函数的关键在于求解
f(pk)
,其它时候我们可以直接利用积性求解,而具体的
f(pk)还是要根据f的性质求解