素数:一个数的因子除1和它本身之外没有其他因子,这样的数称之为素数,合数概念与素数概念恰好相反,1既不是素数也不是合数。本章主要解决两个问题:1.判断素数 2.较为高效判断一个数是否是素数。
普筛
设要判断的数是n,按照定义K ∈ \in ∈ [2~n)中没有出现n%k==0,则可以认为n是个素数。那么就有如下片段:
bool isprimer(int n)
{
if(n<=1) return false;
for(int i = 2;i<n;i++){
if(n%i == 0) return false;
}
return true;
}
上面的时间复杂度显而易见:O(n),我们继续分析看是否能给它的时间复杂度给降低一点,假设这个数n在2~n中有某一位i,能有n%i == 0,那么意为着n/i = k(k ∈ \in ∈[sqrt(n),n)),因为我们默认是从低往高枚举。其实现在也能感觉出来这是合理的,但还是有点迷,其实很好理解,这么想:sqrt(n)就像一个分水岭,sqrt(n) * sqrt(n) = n,那么一个能被n整除的数i,从低往高枚举时,就有i<=sqrt(n),又有k ∗ * ∗ i = n,很自然就能得到k>=sqrt(n)。这就意为着,我们没有必要一直枚举到n,只要到sqrt(n)即可。这是它的代码:
bool isprimer(int n)
{//sqrt()里面放的是浮点数,用1.0转,但在后面要对它向下取整,所以转回来用int
if(n<=1) return false;
int k = int sqrt(1.0*n);
for(int i = 2;i<=k;i++){
if(n%i == 0) return false;
}
return true;
}
//也有人用n/2做边界的,但是sqrt(n)的操作也并不麻烦,随意,开心就好
//当然你快乐的时候也可以在不越界的情况下用i*i<n,记得加<math.h>
上面算法的时间复杂度很好看出来:O(sqrt(n))
为啥要叫筛:筛可以看做一个漏网,筛除不是素数的剩下的就是想要的目标素数,筛法在建素数表时用到。接下来介绍用上面的介绍的方法,来建立第一个素数表,因为它很直接,就叫它普筛。 素数表是啥,你可以从下面的代码中了解。
const int maxn = 102;
int prime[maxn],pNum = 0;//prime中放着2~maxn间的素数,pum表示prime中素数的个数
bool p[maxn]={0};
void Find_prime()
{
for(int i = 2;i<=maxn;i++){
if(isprimer(i)){
prime[pNum++] = i;
p[i] = true;//p[i]为true表示i为素数,之后判断素数时,用if(p[i])就能判断了
}
}
}
上面的时间复杂度分析一下:每一个数判断是否为素数时间复杂度为:O(sqrt(n)),要对1~maxn进行判断,时间复杂度为O(n),而它又包括判断,所以总的时间复杂度为:O(n*sqrt(n)),对复杂度不超过 1 0 5 10^5 105的大小是没问题的。
埃筛
上面的时间复杂度只能针对 1 0 5 10^5 105级别的,接下来介绍一个时间复杂符更优的算法:埃拉托色尼筛法。回想一下:上面的时间复杂度是哪来的,1.对1~n进行遍历2.每遍历一个数,就对它进行一次判断是否是素数。能不能把它给简化,哪能简化,OK,介绍完埃拉托色尼筛法思想后再讨论。它的算法思想是这样的:算法从小到大枚举每一个数,对每一个数筛去它(素数,合数就不要用倍数筛了,靠素数的倍数都能给他们筛了)的倍数(肯定倍数最大得在n内),那么剩下的就是素数了。可以这么理解:当从小到大到达某数a时,如果a没有被前面步骤的数筛去,那么a一定是素数。如果a不是素数,那么a一定有小于a的素因子,这样在之前的步骤中a一定会被筛掉。故当枚举到a,它还没被筛掉,a就一定是素数。这是它的算法:
const int maxn = 101;//表长
int primer[maxn],pNum=0; //primer数组存放所有素数,pNum 表示素数的个数
bool p[maxn]={0};//false 表示改为未被筛除,true 表示已被筛除
void Find_primer()
{
for(int i = 2;i<n;i++){//从2开始,循环条件不能写成i<=maxn(可能越界)
if(p[i] == false){ //i是素数
primer[pNum++] = i; //把素数i存到primer数组中
for(int j = i+i;j<maxn;j+=i){
//筛去所有i的倍数,循环条件不能写成j<=maxn(时刻注意数组下标越界的问题)
p[j] = true;
}
}
}
}//细节较多,注意下
继续接着刚刚话题讨论下:埃拉托色尼筛法在普筛的基础上做了哪些改进,大体上还是枚举,所以外层的O(n)没动,变得是判断素数的策略发生了改变,从对每一个数都判断一下是否为素数,到用素数们的倍数筛掉1~n中的合数,巧妙的应用了这个话:当从小到大到达某数a时,如果a没有被前面步骤的数筛去,那么a一定是素数。如果a不是素数,那么a一定有小于a的素因子,这样在之前的步骤中a一定会被筛掉。故当枚举到a,它还没被筛掉,a就一定是素数。仔细想想,发现这不有手就能出来这个结论,用到了刚刚一点sqrt(n)中的一些理念,这个对其进行深入理解发现其本质后提出的,所以知识点之间不是独立的,分开来看知识点永远不会碰到问题的本质,顶多知型,知意就不大可能了,只有进行交叉分析,才能对问题有更深入的了解。
时间复杂度为O(n log log(n))
欧筛
说完了普筛,埃筛后,就顺便提提欧筛(主要是它不太好理解)吧,欧筛的时间复杂度在O(n),分析埃筛可知:有一些合数被重复筛除,比如:12 = 34=26,被2和3重复筛选了。而欧筛不会出现这种情况。先给个视屏(推荐),可以直接看完,再来体会下或者直接划走写写算法去。欧式筛法 博主讲的很容易理解。
现在介绍下其中最为重要的两步:1.一个合数与一个素数的乘积可以转化为一个更大的合数的一个更小素数的乘积。2.当i%prime[ j ] == 0时,i=p(最小质因子) * q,i(合数) * prime[j+1](质数)= p(最小质因子)* (q * prime[ j+1 ] )(合数),这就是算法要保证的第一步,也是像视屏里的12=26,12是被6给筛除的,而不是12=43给4筛除的,因为4%2==0,在回上面看下欧式筛法需要满足的条件,你就能理解了。
const maxn = 1000001
int prime[maxn];//记录素数
int flag[maxn];//= true,表示为合数
int s,n;//s表示素数的个数,n表示素数表的范围
void Find_prime()
{
for (int i = 2; i <=n;i++){
if(!flag[i])
prime[++s] = i;
for (int j = 1; j <=s && i * prime[j] <= n;j++){
flag[i * prime[j]] = true;
if(i%prime[j] == 0) //数学证明来的
break;
}
}
}
//上面的是欧筛最简单的模型,相信懂了这个后看大佬们的写法,应该能懂一点了。