【Conyrol】素数筛法

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/Conyrol/article/details/83214050

问题 1:
现给出一个数字 n ,求从 2n 的所有素数
( n >= 2 )

目录

暴力硬怼

可以看到如果不计时间的话,这是一道很水的题,暴力即可

#include<cstdio>
int n; 
bool prime(int Num){
  bool b = 1;
  for(int i=2; i<Num; i++) if(!(Num%i)) b = 0;
  return b;
}
int main(){
  scanf("%d",&n);
  for(int i=2; i<=n; i++) if(prime(i)) printf("%d\n",i);   
  return 0;
}

大体分为两个部分

  1. 枚举范围里的每个数
  2. 判断这个数是否是素数,枚举这个比这个数小的数来膜它(-1s),如果能等于0,则不是质数(不包括1)

这样的时间复杂度是 O ( n 2 ) (n^2)
数据大一点就会超时

但是实际上,在判断某一个数 n 是否为质数时,我们第二步只需要枚举到向下取整的 n \bm{\sqrt n} 即可,因为对于一个数来说,它们都可以被拆成 n = n n \bm{ n=\sqrt n\cdot\sqrt n} 的形式
所以说如果这个数不是素数,那它必定在 n \bm{\sqrt n} 之前有一个数能膜出 0 ,之后也有一个与之对应的数能膜出 0 ,故我只需要枚举 n \bm{\sqrt n} 之前的数来判断 n 是否为质数

#include<cstdio>
#include<cmath>
int n; 
bool prime(int Num){
  bool b = 1;
  for(int i=2; i<sqrt(Num); i++) if(!(Num%i)) b = 0;
  return b;
}
int main(){
  scanf("%d",&n);
  for(int i=2; i<=n; i++) if(prime(i)) printf("%d\n",i);   
  return 0;
}

的时间复杂度是 O ( n n ) (n\cdot\sqrt n)
数据大一点依旧会超时,不过对于大部分情况下足够了

接下来我们来介绍一下素数筛法,没错上面那两个都是打酱油的

埃氏筛法

素数筛法我个人认为可以理解成典型的用空间换时间,我们都知道一个数的倍数必定是合数,那我们就可以设置一个 bool 数组,来储存某个数是否是素数的逻辑状态,然后把素数的倍数都设置成 1 (假如说1代表合数的话)

如下图

2 3 4 5 6 7 8 9 10 11 12 13 14 15
2 0 1 1 1 1 1 1
3 0 1 1 1 1
5 0 1 1
7 0 1
0 0 1 0 1 0 1 1 1 0 1 0 1 1
#include<cstdio>
int n; 
bool prime[1000001];
int main(){
  prime[1]=1;
  scanf("%d",&n);
  for(int i=2; i<=n; i++) 
      if(!prime[i])
         for(int j=2; i*j<=n; j++)
             prime[i*j]=1;
  for(int i=2; i<=n; i++) if(!prime[i]) printf("%d\n",i);
  return 0;
}

可以看到这样就筛出范围里的所有质数了,时间复杂度 O ( n ln ln n ) (n\cdot\ln \ln n)
但是仔细看会发现有很多数被多次遍历,这样就提高了时间复杂度
如果我们每个数只遍历一边那岂不就是 O ( n ) (n) 了,此时就需要用到欧拉筛法

欧拉筛法

根据质因数分解定理,我们可以知道合数N可以被分解为有限个质数的乘积形式 N = P 1 a 1 × P 2 a 2 × P 3 a 3 . . . × P n a n N=P_1^{a_1}\times P_2^{a_2}\times P_3^{a_3}...\times P_n^{a_n}
其中 P 代表不同质数, N 代表某个合数

那我们先看一下上一种筛法到底重复筛了什么数?

2 3 4 5 6 7 8 9 10 11 12 13 14 15
2 0 1 1 1 1 1 1
3 0 1 1 1 1
5 0 1 1
7 0 1
0 0 1 0 1 0 1 1 1 0 1 0 1 1

我们先只看2,3,5,7遍历的重复数
其中有
2 × 3 2\times 3 3 × 2 3\times 2
3 × 4 3\times 4 2 × 6 2\times 6
2 × 5 2\times 5 5 × 2 5\times 2
2 × 7 2\times 7 7 × 2 7\times 2
3 × 5 3\times 5 5 × 3 5\times 3
可以分为两类,一类是顺序颠倒,一类是 3 × 4 3\times 4 2 × 6 2\times 6 这种不唯一分解
顺序颠倒很简单,我们可以用

2 × 2 2\times2
3 × 2 3\times 2 3 × 3 3\times 3
4 × 2 4\times 2 4 × 3 4\times 3 4 × 4 4\times 4
5 × 2 5\times 2 5 × 3 5\times 3 5 × 4 5\times 4 5 × 5 5\times 5
6 × 2 6\times 2 6 × 3 6\times 3 6 × 4 6\times 4 6 × 5 6\times 5 6 × 6 6\times 6

每一次枚举的数一定不超过它本身,这样就不会有顺序颠倒导致重复的问题(一行一行看,不是一列一列看Orz)

接下来我们看一下不唯一分解怎么解决
3 × 4 3\times 4 2 × 6 2\times 6 举例
我们通过质因数分解定理可以知道
它们不过是同一种形式的不同表达
12 = 2 2 × 3 1 = ( 2 × 2 ) × 3 = ( 2 × 3 ) × 2 12=2^{2}\times 3^{1}=(2\times 2)\times 3=( 2\times 3)\times2
而欧拉筛法则会只选择最小的素数来完成分解,比如说选择 2 × 6 2\times 6 而不是 3 × 4 3\times 4
那怎么做到?
第一步:我们要保证我们的式子至多有一个合数,因为有两个合数的话都得分解,很难判断,那就直接只乘素数

2 × 2 2\times2
3 × 2 3\times 2 3 × 3 3\times 3
4 × 2 4\times 2 4 × 3 4\times 3
5 × 2 5\times 2 5 × 3 5\times 3 5 × 5 5\times 5
6 × 2 6\times 2 6 × 3 6\times 3 6 × 5 6\times 5

第二步:看4那一行,我们知道 4 实际上就是 2 × 2 2\times 2 得来的,那么我继续往右枚举势必会产生例如
4 × 3 = ( 2 × 2 ) × 3 4\times 3=(2\times 2)\times 3 6 × 2 6\times 2 产生重复
4 × 4 = ( 2 × 2 ) × 4 4\times 4=(2\times 2)\times 4 8 × 2 8\times 2 产生重复
所以说我用枚举出来的当前数 N (就是每行的第一个数)膜上比这个数小的素数(用从小到大的素数来膜),如果这个数可以被膜开,那就证明这个数是合数,那么我继续往下枚举这一行,就会跟以后的式子产生重复,而且你看我每行式子的第二个数不就是从小到大的素数吗!

2 × 2 2\times2
3 × 2 3\times 2 3 × 3 3\times 3
4 × 2 4\times 2
5 × 2 5\times 2 5 × 3 5\times 3 5 × 5 5\times 5
6 × 2 6\times 2
9 × 2 9\times 2 9 × 3 9\times 3

第9行比较特殊,特殊在它第一次用 9%2 没问题,9%3 电脑才发现9是个合数XD,这样 9 × 2 9\times 2 这类的数真的没问题么?
当然没问题!
因为 9 × 2 9\times 2 这种数实际上就是 6 × 3 6\times 3 的相同形式,在上面 6 × 3 6\times 3 已经让出了位置,所以不用担心重复,或者说我们再举一个例子
9 × 5 9\times 5 让出了自己的位置,给的是 15 × 3 15\times 3 ,上面的数都在给下面的数让出位置,而这个让出位置的分界正是N第一个被素数膜开的位置

那么明白了具体的原理,就用代码来实现它吧

#include<cstdio>
int prime[1000001],n,jShu;        // prime[]来存素数 
bool B[1000001];
int main(){
  scanf("%d",&n);
  for(int i=2; i<=n; i++){
      if(!B[i]) prime[jShu++] = i;    //是0就说明是素数,因为是从2顺序运行,没有必要担心选到合数 
      for(int j=0; i*prime[j]<=n&&j<jShu; j++){
          B[i*prime[j]] = 1;             //设成1,i*prime[j]这个是合数 
          if(i%prime[j] == 0) break;     //发现后面不行,跳出 
      }
  }
  for(int i=0; i<jShu; i++) printf("%d\n",prime[i]);
  return 0}

通过时间函数可以看出来素数筛法要比暴力枚举快很多,但埃氏筛法和欧拉筛法基本差不多,只有数据极大的时候才有一点差距

猜你喜欢

转载自blog.csdn.net/Conyrol/article/details/83214050