问题 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
2
)
(n^2)
(n2)
数据大一点就会超时
但是实际上,在判断某一个数 n 是否为质数时,我们第二步只需要枚举到向下取整的
n
\bm{\sqrt n}
n 即可,因为对于一个数来说,它们都可以被拆成
n
=
n
⋅
n
\bm{ n=\sqrt n\cdot\sqrt n}
n=n⋅n 的形式
所以说如果这个数不是素数,那它必定在
n
\bm{\sqrt n}
n 之前有一个数能膜出 0 ,之后也有一个与之对应的数能膜出 0 ,故我只需要枚举
n
\bm{\sqrt n}
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)
(n⋅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)
(n⋅lnlnn)
但是仔细看会发现有很多数被多次遍历,这样就提高了时间复杂度
如果我们每个数只遍历一边那岂不就是 O
(
n
)
(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}
N=P1a1×P2a2×P3a3...×Pnan
其中 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
2×3 和
3
×
2
3\times 2
3×2
3
×
4
3\times 4
3×4 和
2
×
6
2\times 6
2×6
2
×
5
2\times 5
2×5 和
5
×
2
5\times 2
5×2
2
×
7
2\times 7
2×7 和
7
×
2
7\times 2
7×2
3
×
5
3\times 5
3×5 和
5
×
3
5\times 3
5×3
可以分为两类,一类是顺序颠倒,一类是
3
×
4
3\times 4
3×4 和
2
×
6
2\times 6
2×6这种不唯一分解
顺序颠倒很简单,我们可以用
2 × 2 2\times2 2×2 | ||||
---|---|---|---|---|
3 × 2 3\times 2 3×2 | 3 × 3 3\times 3 3×3 | |||
4 × 2 4\times 2 4×2 | 4 × 3 4\times 3 4×3 | 4 × 4 4\times 4 4×4 | ||
5 × 2 5\times 2 5×2 | 5 × 3 5\times 3 5×3 | 5 × 4 5\times 4 5×4 | 5 × 5 5\times 5 5×5 | |
6 × 2 6\times 2 6×2 | 6 × 3 6\times 3 6×3 | 6 × 4 6\times 4 6×4 | 6 × 5 6\times 5 6×5 | 6 × 6 6\times 6 6×6 |
每一次枚举的数一定不超过它本身,这样就不会有顺序颠倒导致重复的问题(一行一行看,不是一列一列看Orz)
接下来我们看一下不唯一分解怎么解决
以
3
×
4
3\times 4
3×4 和
2
×
6
2\times 6
2×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
12=22×31=(2×2)×3=(2×3)×2
而欧拉筛法则会只选择最小的素数来完成分解,比如说选择
2
×
6
2\times 6
2×6而不是
3
×
4
3\times 4
3×4
那怎么做到?
第一步:我们要保证我们的式子至多有一个合数,因为有两个合数的话都得分解,很难判断,那就直接只乘素数
2 × 2 2\times2 2×2 | ||||
---|---|---|---|---|
3 × 2 3\times 2 3×2 | 3 × 3 3\times 3 3×3 | |||
4 × 2 4\times 2 4×2 | 4 × 3 4\times 3 4×3 | |||
5 × 2 5\times 2 5×2 | 5 × 3 5\times 3 5×3 | 5 × 5 5\times 5 5×5 | ||
6 × 2 6\times 2 6×2 | 6 × 3 6\times 3 6×3 | 6 × 5 6\times 5 6×5 |
第二步:看4那一行,我们知道 4 实际上就是
2
×
2
2\times 2
2×2 得来的,那么我继续往右枚举势必会产生例如
4
×
3
=
(
2
×
2
)
×
3
4\times 3=(2\times 2)\times 3
4×3=(2×2)×3 跟
6
×
2
6\times 2
6×2 产生重复
4
×
4
=
(
2
×
2
)
×
4
4\times 4=(2\times 2)\times 4
4×4=(2×2)×4 跟
8
×
2
8\times 2
8×2 产生重复
所以说我用枚举出来的当前数 N (就是每行的第一个数)膜上比这个数小的素数(用从小到大的素数来膜),如果这个数可以被膜开,那就证明这个数是合数,那么我继续往下枚举这一行,就会跟以后的式子产生重复,而且你看我每行式子的第二个数不就是从小到大的素数吗!
2 × 2 2\times2 2×2 | ||||
---|---|---|---|---|
3 × 2 3\times 2 3×2 | 3 × 3 3\times 3 3×3 | |||
4 × 2 4\times 2 4×2 | ||||
5 × 2 5\times 2 5×2 | 5 × 3 5\times 3 5×3 | 5 × 5 5\times 5 5×5 | ||
6 × 2 6\times 2 6×2 | ||||
… | ||||
9 × 2 9\times 2 9×2 | 9 × 3 9\times 3 9×3 |
第9行比较特殊,特殊在它第一次用 9%2 没问题,9%3 电脑才发现9是个合数XD,这样
9
×
2
9\times 2
9×2 这类的数真的没问题么?
当然没问题!
因为
9
×
2
9\times 2
9×2 这种数实际上就是
6
×
3
6\times 3
6×3 的相同形式,在上面
6
×
3
6\times 3
6×3 已经让出了位置,所以不用担心重复,或者说我们再举一个例子
9
×
5
9\times 5
9×5 让出了自己的位置,给的是
15
×
3
15\times 3
15×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;
}
通过时间函数可以看出来素数筛法要比暴力枚举快很多,但埃氏筛法和欧拉筛法基本差不多,只有数据极大的时候才有一点差距