----- update on 2020 03 03 ------------
0.总结
0.1 线性筛素数
#include<iostream>
#include<cstdio>
using namespace std;
const int maxN = 1005;
const int maxC = 1005;//存储maxN范围内的素数个数
int arr[maxN];//原始数组
int prime[maxC];//存储素数
int main(){
int n,q;
cin >> n>> q;
fill(arr,arr+maxN,1);
int count = 1;
for(int i = 2;i <= n;i++){
if(arr[i]!=0){
prime[count++] = i;
}
for(int j = 1; j<=count && i * prime[j] <=n; j++){
arr[i*prime[j]] = 0;//不是素数
if(i % prime[j] == 0){
break;
}
}
}
int num;
while(q--){
cin >> num;
cout << prime[num]<<"\n";
}
}
-------- ----- ----- ----- ----- ----- ----- ----- ----- -----
1.前言
由于最近在刷PAT题,我想想这些天接触到的一些新的知识,如下:
- (1)如何判断一个数是否为素数?
- (2)如何求出一个数字范围内的所有素数?并且使得这个过程尽量高效?
假设读者都具有以下的知识:
- (1)一个数是素数的条件是:它只能被1和其本身整除。
- (2)1不是素数!
2.设计算法
- (1)暴力法
我在这里姑且称其为暴力法,其实这是最为笨拙的一中方法,代码如下:
#include <stdio.h>
void isPrime(int number){
int j ,i = 0 ;
for(j = 2;j <= number;j++){
for(i = 2;i < j;i++){
if(j % i == 0){
break;
}
}
if(i == j){
printf("%d ",j);
}
}
}
int main(){
isPrime(20);
}
我们对上面的代码稍作分析,就会发现,我们有很多工作是多于的,比如说,我们对一个数a来说,它除以比其1/2还要大的数时,其余数肯定不为0,所以我们可以将代码这么修改:
#include <stdio.h>
void isPrime(int number){
int j ,i = 1 ;
for(j = 2;j <= number;j++){
for(i = 2;i <= j /2 ;i++){
if(j % i == 0){
break;
}
}
if(i > j/2){
printf("%d ",j);
}
}
}
int main(){
isPrime(20);
}
需要注意的地方是:for()循环结束的条件,是i<=j/2,和判断是否输出的条件if(i > j/2),这里需要针对4这个数进行修改。虽然将判断的数据缩小了一半,但是这仍然是不够得,在一般的算法题中,这样还是会超时。
下面我们对其再进行优化一下。
- (2)优化后的暴力枚举
现在假设有一个数A,它有除1和本身之外的两个因子,分别称其为m,n即有m*n=A,那么肯定有m<sqrt(A)&&n>sqrt(A)或者m>=sqrt(A)&&n<=sqrt(A)。这里的sqrt(A)代表的是A的平方根。因为一个数的两个因子肯定是在这个数的平方根的两边。所以我们只需要遍历平方根左边的所有数字,如果有一个数字是其因子,那么在其平方根右边,肯定存在一个数也是其因子。所以就有了下面的这个算法:
#include <stdio.h>
#include <math.h>
void isPrime(int number){
int j ,i = 1 ;
for(j = 2;j <= number;j++){
for(i = 2;i < sqrt(j) ;i++){
if(j % i == 0){
break;
}
}
if(i > sqrt(j)){
printf("%d ",j);
}
}
}
int main(){
isPrime(20);
}
但是当我们处理一个大于10^5的素数时,这样的算法还是不够快,于是有了下面的这样的神奇算法。
- (3)筛法
筛法有很多种,这里讲最重要的,也是复杂度最低的线性筛法。线性筛法成立的基础是:唯一分解定理。
【下面所要说的因子均是除了1和本身之外】【我们给定一个数字范围,比如求20之前的所有素数】我们观察数字2,在此之前没有因子,所以其是素数,输出。---->推导出所有公因子中有2的数则不是素数,于是
- step1》去掉了4,6,8,10,12,14,16,18,20...这些数
接着循环到数字3,发现它并没有被去掉,所以其是素数,输出。接着按照上面的方法,在这个数列中去除因子为3的数,于是
- step2》去掉了9,15...这些数。
接着往下找第一个没有被去除的数,为数字5,所以为素数,输出
……
如此循环直到最后一个数字被处理。得到算法如下:
#include <stdio.h>
#include <math.h>
void isPrime(int number){
int j ,i = 1 ;
int array[100];
for (i = 0;i<=number;i++){
array[i] = i;
}
for(j = 2;j <= number;j++){ //求20之前的素数
if(array[j]!=0){
printf("%d ",array[j]);
}
for(i = 2;i <= number ;i++){
if( i % j == 0){
array[i] = 0;
}
}
}
}
int main(){
isPrime(20);
}
运算结果:
上面的筛法思想是正确的,但是代码实现真的好么?如果不好,那么好的算法该怎么实现呢?在说怎么实现之前,先说出上面的代码的问题(很明显,这里的错误有很多,下面会逐一列举。)
问题1:就像底部的第一条评论说的那样,这个筛法不是说优于其他算法么?但为啥这里的“筛法”复杂度还是O(n^2)?原因只有一个:那就是博主太菜了(过于真实)!!之前虽然理解了筛法的过程,但其实现是不正确的。
问题2:应该用找出来的素数进行筛选,而不是再次循环用j过滤(否则就与朴素的暴力无异了)。也就是说,上述的这几行代码是存在问题的。
if( i % j == 0){
array[i] = 0;
}
那么正确的筛法该如何实现?
筛法的主要思想如下:
(1)用素数筛掉不确定的数【切记,这里是用素数筛!】
(2)接下来第一个未筛掉的数就是素数(如:序列2,3,4,5,6。且2是素数,则接下来第一个未删掉的数就是3,故3就是素数,4会被2筛掉...)
(3)如何保证每个数都只筛一遍?比如数字6可以被素数2筛掉,也可以被素数3筛掉,那么到底怎么筛的呢?
这里采取的方法是用最小的素数筛,即用2筛。
如果上述实现三点能够实现,我们则可以得到一个近乎O(n)复杂度的求区间素数的样例。代码如下【结合络谷P3383写出代码】。
#include<iostream>
#include<cstdio>
using namespace std;
const int maxN = 1005;
const int maxC = 1005;//存储maxN范围内的素数个数
int arr[maxN];//原始数组
int prime[maxC];//存储素数
int main(){
int n,q;
cin >> n>> q;
fill(arr,arr+maxN,1);
int count = 1;
//线性筛素数
for(int i = 2;i <= n;i++){
//说明是素数
if(arr[i]!=0){
prime[count++] = i;
}
//1.通过prime[j](是素数)筛掉其它的数
//2.j的下标小于count;
//3.i*arr[j]的范围小于指定范围n
for(int j = 1; j<=count && i * prime[j] <=n; j++){
arr[i*prime[j]] = 0;//不是素数
//如果i可以整除prime[j],那么直接跳出
if(i % prime[j] == 0){
break;
}
}
}
int num;
while(q--){
cin >> num;
cout << prime[num]<<"\n";
}
}
为了更好的理解这段代码,应该自己debug一下整个过程。下面我给出我的一些理解点。
- i用于大于prime[j]。为啥?因为prime[j]是i范围内的素数。
需要微观上把握代码细节,即该行代码有什么意义?为什么这么写?而不是东拼西凑的去尝试得出正确解。
arr[i*prime[j]] = 0;//不是素数
上面这句代码的作用是:prime[j]是i*prime[j]的最小公因子。
if(i % prime[j] == 0){
break;
}
上面这三行代码是筛法的精髓。
- 如果prime[j]能够整除i,(prime[j]是素数,i是合数)。可以知道后面的序列中能够被i整除的肯定可以被prime[j]整除。所以对于后面的序列中是i倍数的那些数,我们可以且应该用prime[j]删除(因为prime[j]是素数且小于i,这样就能保证只删一次)。
筛法求素数的模板,对每名algorithmer都是基础,应该牢记。