在上上个星期,我们学习了如何判断一个数是否是素数
大家可以先简单的回顾一下:质数判断
在文章的最后,我提到了一个思想,判断一个数是否为素数的最好方法是判断小于√n的素数中有没有可以整除n的。
因为一个数如果不可以被一个数整除,那么就肯定不可以被这个数的倍数整除,而素数因为没有任何约数,所以我们要对所有的素数进行判断
进而,我们不需要判断所有的合数,因为合数必定是至少一个素数的倍数
?你问我为什么???
一个数的约数的约数一定是这个数的约数,那么如果一个数的约数都是合数,就必定还有一些约数的约数还没有被找到,那么说明这些约数不是当前这个数的全部约数,也就说明一个数的约数不可能全都是合数,也就证明了我们刚才的命题。
好的,现在我们已经大致上证明了为什么判断一个数是否是素数只需要判断比√n小的素数中有没有可以整除它的。(如果不明白就再看一次上一篇关于素数判断的文章和上文)
好了,现在我们来看题
Galon同学因为在上课睡觉,老师给他出了一道题,并且表示做不出来以后的GPA就是零分,但是因为Galon同学平时只知道学习python而疏忽了算法,对这道题一点思路都没有,身为Galon同学的同桌,你是他唯一可以依靠的人了,你可以帮帮他吗?
(不可以,Galon同学,你要学会独立思考【滑稽】)
因为设定原因,我们不能不做这题,那么让我们一步一步来理清思路
首先,我们从最简单的思路出发。
要判断一共有多少个素数,只需要从1到N循环一遍,然后依次判断每一个数是不是素数,是的话就让答案+1
因为判断一个数是不是素数的时间复杂度是O(√n)
而我们要从1到N依次判断,所以这个算法的时间复杂度是O(n√n/3),约等于O(n√n)
虽然这是最简单的算法,但是我们还是写一下吧。
代码如下:
#include#include#include#include#include#include#includeusing namespace std;inline int rd(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f;}inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return ;}int n,ans;inline int check(int x){ int f=0; if(x==2||x==3){//特殊判断 return 1; } if(x%2==0||x%3==0){//筛掉形如6x+2,6x+3,6x+4的数 return 0; } for(int i=6;i-1<=sqrt(x);i+=6){//每次加6,判断两侧是否可以整除n if(x%(i-1)==0||x%(i+1)==0){ f=1;//标记n可以被整除 break; } } if(f) return 0;//如果标记改变,说明n可以被一个其他的数整除,说明n不是素数 return 1;//如果标记没有改变,说明从2到n-1没有一个数可以整除n,说明n是素数}int main(){ n=rd(); for(int i=2;i<=n;i++){//从2开始,因为1既不是合数也不是素数 if(check(i)){ ans++; } } cout< return 0;}
然后我们来改进这个算法,当我们判断x是否为素数的时候,我们已经判断了所有小于x的整数是否为素数,于是我们可以将前面的素数都保存在一个数组中,然后在判断x的时候,循环之前的所有的素数,然后判断其中有没有可以整除x的数,如果都不能整除x,说明x也是素数,然后我们就要把x存进数组,用于之后的判断。
因为每次判断一个数的时候只需要循环它前面的素数的个数次,这个算法的时间复杂度就变成了O(n*(n之前的素数的个数))
下面给出代码:
#include#include#include#include#include#include#includeusing namespace std;inline int rd(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f;}inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return ;}int n,ans;int pos;int p[100006];//p数组中存的是素数,pos存的是当前的素数个数 inline int check(int x){ for(int i=1;i<=pos&&p[i]<=sqrt(x);i++){ if(x%p[i]==0){ return 0; } } return 1;}int main(){ n=rd(); for(int i=2;i<=n;i++){//从2开始,因为1既不是合数也不是素数 if(check(i)){ ans++; p[++pos]=i; } } cout< return 0;}
很显然,在这个代码中,每一个数要被判断小于它的素数的个数次,但是我们知道,在其中起到作用的只有可以整除x的素数,所以我们要尽量省去判断其他素数的次数。
于是我们就可以想到一个新的算法:(来自百度百科)
埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。
这段文字其实已经说到了这个算法的精髓,当我们要判断小于n的素数时,我们只需要剔除其中小于√n的素数的所有的倍数即可,所以我们可以直接从素数出发,删除那些倍数。
我们首先从2开始,用一个循环删去2小于n的所有的倍数,然后现在剩下的最小的自然数就是下一个素数,因为这说明小于它的所有质数都不可以整除它,然后我们就循环这个新的素数的倍数并且删掉它们。
而如何完成删除这个操作呢?
我们可以定义一个数组book,book[i]表示i是否是一个素数,如果i是一个素数那么我们就把book[i]的值就为0,反之则为1。
开始的时候,我们先定义book[2]=0,当我们循环2的倍数的时候,我们将所有的book[2k]赋为1(2k<=n k为正整数)
删除所有2的倍数后,我们就去寻找下一个book值为0的数,就是我们下一个处理的素数。
这个代码的时间复杂度为n/2+n/3+n/5+……+n/(小于√n的最大的素数)
而这样算下来约等于
具体为什么算下来等于这个数我也不太清楚,想知道的可以去问lgx同学或者身边的数学大佬。
#include#include#include#include#include#include#includeusing namespace std;inline int rd(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f;}inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return ;}int n,ans;int book[1000006];//存的是每个数是否为素数 inline int solve(int x){ for(int i=2;i*x<=n;i++) book[i*x]=1;//每次删去小于n的所有的当前素数的倍数 }int main(){ n=rd(); for(int i=2;i<=n;i++){//从2开始,因为1既不是合数也不是素数 if(book[i]==0){ ans++; solve(i); } } cout< return 0;}
然后我们再来想,如果我们要判断一个数是不是素数,只需要判断这个数是不是合数,而判断一个数是不是合数,只需要找到一个可以整除它的不是1和它本身的数,而更进一步来讲,我们只需要找到一个小于√n的可以整除它的素数,如果找到了,说明它不是素数。
在刚才的埃氏筛法中,每个数都会别小于√x的素数删去一遍
我们能不能让一个数只被判断一次呢,这样我们就可以实现线性的时间复杂度,也就是理论上的O(n)
这样的神级算法是存在的,也是我们今天的主角,欧拉筛
和埃氏筛一样,我们同样利用一个数组book来存一个数是不是素数,是的话我们就要把它的倍数删去
不同的是,我们不是直接把一个素数的倍数删去,而是在其中加一些判断,保证一个数不会被重复的删掉两次。
如何做到这一点,我们用另一个数组p来存之前所有筛出来的素数
当我们判断x是不是素数时,我们循环之前找到的所有素数,并且删去x*p[i]
当然,我们并不是将所有的素数都删去,而是要附加一些条件
(1)
当我们发现x*p[i]大于n时,我们就退出素数的循环,因为之后的素数都大于p[i],超过了n,也就超出了我们需要判断的范围
(2)
如果x是p[i]的倍数,我们也要退出素数的循环,因为之后的素数如果继续删,就会出现重复,我们在这里用数学证明:
因为x=p[i]*v
所以x*p[i+1]=(p[i]*v)*p[i+1]
我们又知道p[i]
所以p[i+1]*x在之后一定会以被删去,以p[i]*x'的形式,x'=x*p[i+1]
以此类推,之后的素数就都不用判断了
于是我们就得到了最后的算法
下面给出代码,请大家认真阅读注释:
#include#include#include#include#include#include#includeusing namespace std;inline int rd(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f;}inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return ;}int n,ans;int book[1000006];//存的是每个数是否为素数 int pos;int p[100006];//存的是当前找的的素数,从小到大排列 int main(){ n=rd(); for(int i=2;i<=n;i++){//从2开始,因为1既不是合数也不是素数 if(book[i]==0){ ans++; p[++pos]=i;//记录新的素数 } for(int j=1;j<=pos;j++){ if(i*p[j]>n) break;//如果超出了边界,之后的就都不用判断了 book[i*p[j]]=1; if(i%p[j]==0) break;//如果可以整除,说明之后会判断到 } } cout< return 0;}
其实不难发现,我们判断素数的速度虽然越来越快,但是需要的空间也越来越多,第一个算法我们需要存储的只有一个整数n,但是对于欧拉筛,我们就要利用一个大小为n的数组。
同时我们可以完成的操作也更多,比如说后面两种算法,不仅仅可以求出素数的个数,也可以用来判断从1到n任意一个数是不是素数,而前两种则做不到。
随着时间的升级,我们要在空间上花费更多,大家在使用的时候可以根据题目要求选择。
以上就是素数筛的全部内容了,大家有没有在其中感受到思维升级的乐趣呢
如果你喜欢这篇文章,记得关注转发哦!