问题 1:
现给出一个数字 n ,求从 2 到 n 的所有素数
( 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;
}
大体分为两个部分
- 枚举范围里的每个数
- 判断这个数是否是素数,枚举这个比这个数小的数来膜它(-1s),如果能等于0,则不是质数(不包括1)
这样的时间复杂度是 O
数据大一点就会超时
但是实际上,在判断某一个数 n 是否为质数时,我们第二步只需要枚举到向下取整的
即可,因为对于一个数来说,它们都可以被拆成
的形式
所以说如果这个数不是素数,那它必定在
之前有一个数能膜出 0 ,之后也有一个与之对应的数能膜出 0 ,故我只需要枚举
之前的数来判断 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
数据大一点依旧会超时,不过对于大部分情况下足够了
接下来我们来介绍一下素数筛法,没错上面那两个都是打酱油的
埃氏筛法
素数筛法我个人认为可以理解成典型的用空间换时间,我们都知道一个数的倍数必定是合数,那我们就可以设置一个 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
但是仔细看会发现有很多数被多次遍历,这样就提高了时间复杂度
如果我们每个数只遍历一边那岂不就是 O
了,此时就需要用到欧拉筛法
欧拉筛法
根据质因数分解定理,我们可以知道合数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遍历的重复数
其中有
和
和
和
和
和
可以分为两类,一类是顺序颠倒,一类是
和
这种不唯一分解
顺序颠倒很简单,我们可以用
每一次枚举的数一定不超过它本身,这样就不会有顺序颠倒导致重复的问题(一行一行看,不是一列一列看Orz)
接下来我们看一下不唯一分解怎么解决
以
和
举例
我们通过质因数分解定理可以知道
它们不过是同一种形式的不同表达
而欧拉筛法则会只选择最小的素数来完成分解,比如说选择
而不是
那怎么做到?
第一步:我们要保证我们的式子至多有一个合数,因为有两个合数的话都得分解,很难判断,那就直接只乘素数
第二步:看4那一行,我们知道 4 实际上就是
得来的,那么我继续往右枚举势必会产生例如
跟
产生重复
跟
产生重复
所以说我用枚举出来的当前数 N (就是每行的第一个数)膜上比这个数小的素数(用从小到大的素数来膜),如果这个数可以被膜开,那就证明这个数是合数,那么我继续往下枚举这一行,就会跟以后的式子产生重复,而且你看我每行式子的第二个数不就是从小到大的素数吗!
… | ||||
第9行比较特殊,特殊在它第一次用 9%2 没问题,9%3 电脑才发现9是个合数XD,这样
这类的数真的没问题么?
当然没问题!
因为
这种数实际上就是
的相同形式,在上面
已经让出了位置,所以不用担心重复,或者说我们再举一个例子
让出了自己的位置,给的是
,上面的数都在给下面的数让出位置,而这个让出位置的分界正是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;
}
通过时间函数可以看出来素数筛法要比暴力枚举快很多,但埃氏筛法和欧拉筛法基本差不多,只有数据极大的时候才有一点差距