(这是我这个小白人生的第一篇博客,很光荣的给了数论这个模块,了解我自己,之后一定会回头看自己写的骚东西,所以在这个开头就告诉未来的自己,选择了的路一定要坚持下去,无论前方的路多么难走,切记不忘初心,加油,不放弃!)
下面进入正题:
本篇主要总结各种素数筛法,希望能将这一知识点学通透,并能熟练的运用。
一个大于1的自然数,除了1和他本身,不能整除其他自然数的数,叫做素数。
枚举从2到n-1的所有数,看是否存在被n整除的数,若存在,则不是素数,若不存在,则是素数。
但其实只需枚举根号次,即2到根号n。
原因(自己想的):
2乘n/2等于n,3乘n/3等于n…根号n乘根号n等于n,以此类推,若n整除2成立,则n整除n/2必定也成立,因此只需判定2到根号n这一区间内是否存在被n整除的数,即可判断n是不是素数。
下面是该方法的代码:
bool Is_prime (int n )
{
for (int i = 2 ; i * i <= n ; i++ )
{
if (n % i ==0 ) return 0 ;
}
return true ;
}
那么现在问题来了,如果现在有m个数需要判断是否是素数,然后m<=1000000, n<=100000000 , 此时分析一下以上方法的时间复杂度,很容易分析得出:
一般来说平台一秒能跑大约不到1e8,于是当出现以上那种扯淡的数据的时候,一般规定时间是1秒,就可能会TLE,因此可以得出结论,偷懒不怎么用动脑的方法是行不通的,于是就开始探寻新的方法。
牛逼的方法往往要在后面出,因为是在学习总结,所以一步一步循序渐进的来不断优化和探索。那么就看下一个方法:
埃氏筛法其实很好理解,筛法筛法,顾名思义是要进行筛除,那么具体如何筛除才能留下所要的素数呢。筛除范围是2到n,2是最小的素数,所以将2到n之内2的倍数筛除,那么剩下的最小的数便是3,这时候我们联系上面无脑的单个素数判定方法,就是看2到根号n之内的数是否存在一个能被n整除,这时候对于3,已经被前面筛过的2检测过了,并且留下来了,因此3便是素数。以此类推,再将3的倍数全部筛除,此时剩下的最小的数是5,是被2和3都检测过并且留下的数,因此5也是素数,这样不断筛下去,就能得出2到n之内的素数。
可能会人有疑问,当不断地往后进行筛除步骤时,比如到了17,2到根号17之内的数,还有一个4并没有被检测过,这样会不会出现问题,但是其实4已经被2筛除了,17连2都无法整除,又怎么整除4呢?这样看来,从逻辑上来判断这个埃氏筛法是正确无误的。
(以上筛除原理是我自己的理解,如有不对还请各路大神指正。)
然后我们用代码来实现:
#include
#define N 10000005
using namespace std;
bool Is_prime[N+5];//检测是否被筛除``
int prime[N+5];//放素数的素数表
int cnt=0;//素数个数统计
void aishai(int n)
{
for(int i=0;i<=n;i++)Is_prime[i]=true;//先将所有的数都放在表里,留待后面筛除
Is_prime[0]=Is_prime[1]=false;//0和1都不是素数,提前筛除
for(int i=2;i<=n;i++)//开筛
{
if(Is_prime[i])
{
prime[cnt++]=i;//当前数未被筛除,存入素数表
for(int j=i+i;j<=n;j=j+i)
{
Is_prime[j]=false;//将当前素数的倍数进行筛除
}
}
}
return ;//筛完收工
}
int main()
{
int n;
cin>>n;
aishai(n);
for(int i=0;i<cnt;i++)cout<<prime[i]<<" ";
return 0;
}
试着检验了一下,随便举了个数1000
(其实上面的代码还可以精简一些,比如bool数组完全不用多初始化一遍,false和true反过来用就完事了,void函数return也不用加,但是为了容易理解和emmm博客效果所以就这么写了)
然后分析一下埃氏筛法的时间复杂度,其实我也不太会分析,这里就直接抄学长的了,为O(nloglogn)。可以看出来,相比无脑的方法,明显快多了,那么,还没木有更快的方法呢…当然有了…
下面开始这篇博客的核心方法,也是本菜鸡之前死背下来的方法,近似线性O(n)的欧拉筛法。
首先先来看已经很牛逼的埃氏筛法哪些地方可以进行优化改进,我们可以发现,在每次筛除的时候,其实质因数比较多的数都会被他的多个质因数重复筛除,而这一操作是没有必要的,不需要用他的多个质因数去筛一遍又一遍。因此欧拉筛法相比埃氏筛法改进的最精髓最关键的地方在于,让每个数只被他的最小质因数筛除一次,节省了大量时间,所以才达到了近似O(n)的时间复杂度。
那么如何实现这一操作呢,这里我思考了很久,中间自己傻乎乎的琢磨了半天,其实也没太琢磨透彻。就大概的解读下代码吧。
上代码:
#include
#define N 10000005
using namespace std;
bool Is_prime[N+5];//检测是否被筛除
int prime[N+5];//放素数的素数表
int cnt=0;//素数个数统计
void oulashai(int n)
{
for(int i=0;i<=n;i++)Is_prime[i]=true;//先将所有的数都放在表里,留待后面筛除
Is_prime[0]=Is_prime[1]=false;//0和1都不是素数,提前筛除
for(int i=2;i<=n;i++)
{
if(Is_prime[i])prime[cnt++]=i;//判断是否被筛,放入素数表
for(int j=0;j<cnt;j++)//核心来了
{
if(i*prime[j]>n)break;//如果乘积比范围大,跳出,优化省时间。
Is_prime[i*prime[j]]=false;
if(i%prime[j]==0)break;//见下面详解
}
}
return ;//筛完收工
}
int main()
{
int n;
cin>>n;
oulashai(n);
for(int i=0;i<cnt;i++)cout<<prime[i]<<" ";
return 0;
}
着重解释一下下面这个东西。
for(int j=0;j<cnt;j++)//核心来了
{
if(i*prime[j]>n)break;//如果乘积比范围大,跳出,优化省时间。
Is_prime[i*prime[j]]=false;//(1)标个序号,下面解释用
if(i%prime[j]==0)break;//(2)序号
}
还是自己粗浅的理解,我们注意到,在埃氏筛法中,我们通过一层一层的筛倍数,来实现对整体所有数据的一遍遍筛除,可是在欧拉筛法中,却有所不同(废话)。
模拟代码(1)运行,我们发现,prime[0]=2,把这个单独拿出来,每次都是把i乘prime[0]也就是2这个数筛除,不断向后运行重复这个筛除i乘2的过程,可以发现,我们提前把n的范围内2的所有倍数都筛除了,以此类推,每个素数的倍数往后筛,都会逐渐的筛除,留下的就是素数了。这是(1)这行代码的作用,大神们不要笑我,我开始看不明白还就真的是这行没看懂。
然后再看欧拉筛法的关键性精髓,就是序号(2)这行代码,这个跳出操作。我个人觉得这行代码比(1)容易理解。。。。。我粗浅的理解下,如果满足这个i%prime[j]==0的条件,那么说明此时这个i是当前素数表中某个素数的倍数,而因为从头开始遍历,这个素数正是i的最小质因数,这样就实现了让筛除次数尽可能少,每个数只被筛一遍的操作。
还是举个栗子,2和3的倍数6,如果用埃氏筛法,会被2筛过后,又被3踩一脚。然而用欧拉筛法,在i等于3的时候,i乘2等于6,6被筛除,当后面到i等于6的时候,6余2等于0,直接跳出,不用再筛了。
这样分析下来,好像还有那么几分道理…其实具体的我也没有想明白,但是分析到这个程度已经不影响灵活的应用了。
下面贴一个应用的模板例子吧。(我开始皮了)
中国石油大学(华东)的ACM俱乐部有很多大神,很多可爱的学长学姐。其中有一位贼强的学长大家都叫他楠神,这一天,刚刚进入实验室的小旬同学想要加楠神的qq好友,结果怀着忐忑和激动的心情刚准备发送好友申请,却出现了眼前的这一幕,让小旬很是懵逼,感叹楠神不愧是楠神,真是太强大了,一般人还真的加不上他,小旬心想,用暴力做法岂不是会被他的好盆友小旭鄙视的无地自容?于是小旬决定不用暴力方法解决这个问题。
#include
#define N 10000005
using namespace std;
bool Is_prime[N+5];//检测是否被筛除
int prime[N+5];//放素数的素数表
int cnt=0;//素数个数统计
void oulashai()
{
for(int i=0;i<=1000000;i++)Is_prime[i]=true;//先将所有的数都放在表里,留待后面筛除
Is_prime[0]=Is_prime[1]=false;//0和1都不是素数,提前筛除
for(int i=2;i<=1000000;i++)
{
if(Is_prime[i])prime[cnt++]=i;//判断是否被筛,放入素数表
for(int j=0;j<cnt;j++)//核心来了
{
if(i*prime[j]>1000000)break;//如果乘积比范围大,跳出,优化省时间。
Is_prime[i*prime[j]]=false;
if(i%prime[j]==0)break;//见下面详解
}
}
return ;//筛完收工
}
int main()
{
oulashai();
cout<<prime[77116]<<endl;
return 0;
}