为了求个素数,科学家们也是蛮拼的
0.素数筛选之大难题
题目非常简单:
输入一个N,判断从0~N有多少个素数
至于数据范围:
0≤N≤367356
素数是个迷人的东西,哥德巴赫猜想就诞生在它身上。素数无法使用那些智障般的判断法则,所以素数的判断更令人着迷。从古至今,多少科学家们为此前仆后继,从试数法,到线性筛,一步步彰显人类的智慧与数学的奥妙。
今天,就让我们一起从最远古的试数法开始,追溯前人,学习素数筛选法。
1.试数法
1 bool isPrime(int n){ 2 if(n<2) 3 return false; 4 5 for(int i=2;i<n;i++) 6 if(n%i==0) 7 return false; 8 9 return true; 10 } 11 12 int main(){ 13 int n; 14 scanf("%d",&n); 15 for(int i=2;i<=n;i++) 16 if(isPrime(i)) 17 printf("%d ",i); 18 return 0; 19 }
故名思义,我从1~N把每个数都判断一下,这样的时间复杂度是喜闻乐见的O(n2)。为了让大家更直观的感受O(n2)之快,我们来做个小小的实验。
1 bool isPrime(int n){ 2 if(n<2) 3 return false; 4 5 for(int i=2;i<n;i++) 6 if(n%i==0) 7 return false; 8 9 return true; 10 } 11 12 int size; 13 14 int main(){ 15 clock_t start,end; 16 17 int n; 18 scanf("%d",&n); 19 start=clock(); 20 for(int i=2;i<=n;i++) 21 if(isPrime(i)) 22 // printf("%d ",i); 23 size++; 24 25 end=clock(); 26 27 printf("%lf",(double)(end-start)/CLOCKS_PER_SEC); 28 return 0; 29 }
<附>clock()函数简单介绍
所需头文件:<time.h>或<ctime>
返回值类型:clock_t (long类型);
返回值:从开启这个程序进程到程序调用clock()函数之间的CPU时钟计时单元(clock tick)数(挂钟时间)。
例如:
int size; int main(){ clock_t start,end; int n; scanf("%d",&n); start=clock(); printf("%lf\n",(double)start/CLOCKS_PER_SEC); for(int i=2;i<=n;i++) if(isPrime(i)) // printf("%d ",i); size++; end=clock(); printf("%lf\n",(double)end/CLOCKS_PER_SEC); return 0; }运行结果:
输入:10000
输出:2.354000//从程序开始到start=clock()的用时
2.270000//从程序开始到end=clock()的用时
CLOCKS_PER_SEC:常量,表示每一秒(per second)有多少个时钟计时单元
看不懂倒也没关系,能读数便行
看看运行结果:

经过漫长的等待,我们发现,当N=367356,需要15~16秒钟,才能完成运行。
所以,我们需要优化算法。
2.试数法sqrt()优化
我们仔细思考:
一个合数n=a×b, 那么因数a,b一定存在于两个集合中,也就是[2,√n],[√n +1,n-1]。
如果你没听懂,读者自证不难。
总之:
1 bool isPrime(int n){ 2 if(n<2) 3 return false; 4 5 for(int i=2;i<=sqrt(n);i++) 6 if(n%i==0) 7 return false; 8 9 return true; 10 } 11 12 int size; 13 14 int main(){ 15 clock_t start,end; 16 17 int n; 18 scanf("%d",&n); 19 start=clock(); 20 21 22 for(int i=2;i<=n;i++) 23 if(isPrime(i)) 24 // printf("%d ",i); 25 size++; 26 27 end=clock(); 28 29 printf("%lf",(double)(end-start)/CLOCKS_PER_SEC); 30 return 0; 31 }
这时我们的时间复杂度已经非常可观:

O(√n)
但是,时间是有限的,优化是无限的
3.试数法sqrt()÷2优化
想一想:偶数之中,除了2都是合数。
于是我们可以尝试跳过偶数直接判断。
1 bool isPrime(int n){ 2 if(n<2) 3 return false; 4 for(int i=2;i<=sqrt(n);i++) 5 if(n%i==0) 6 return false; 7 return true; 8 } 9 10 int size; 11 12 int main(){ 13 clock_t start,end; 14 15 int n; 16 scanf("%d",&n); 17 start=clock(); 18 size++; 19 for(int i=3;i<=n;i+=2) 20 if(isPrime(i)) 21 // printf("%d ",i); 22 size++; 23 24 end=clock(); 25 26 printf("%lf",(double)(end-start)/CLOCKS_PER_SEC); 27 return 0; 28 }
耗时有了微小的变化:

4.超高级的The Sieve of Eratosthenes(埃拉托斯特尼筛法)
名字听起来就很不错~
埃氏筛基本思想是这样的,如果我知道一个数 x 是素数,那么它的倍数一定是合数,也就是 2x, 3x, 4x, ... 都是合数。
举例说明:2 是素数,它的倍数们:4、6、8、10 等等,都是合数,这是显而易见的。
假设从起点开始(起点可由要求指定)的所有数都是质数。从起点开始向前搜寻,若为质数,则将其倍数(不超过上界n)标记为非质数。例如2为质数,则标记4,6,8, ...这些2的倍数都为非质数,然后标记下一个……依此类推。
度娘曰:要得到自然数n以内的全部素数,必须把不大于 的所有素数的倍数剔除,剩下的就是素数。
详细过程如下:
现有一个数列[2,25]:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
序列的第一个质数是2;
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
于是我们划去2的所有倍数;序列变成了:
2 3 5 7 9 11 13 15 17 19 21 23 25
如果这个序列中最大数小于最后一个标出的素数的平方,那么剩下的序列中所有的数都是素数,因为25大于2的平方,我们还要继续;
序列的下一个数是3,于是我们划去3的所有倍数:
2 3 5 7 11 13 17 19 23 25
因为25大于3的平方,所以我们继续算;再下一个数是5,于是我们划去5的所有倍数:
2 3 5 7 11 13 17 19 23
此时23小于5的平方,所以剩下的这一溜全是素数。
代码实现:
1 int size; 2 int vis[400010]; 3 4 int main(){ 5 clock_t start,end; 6 7 int n; 8 scanf("%d",&n); 9 start=clock(); 10 11 for(int i=2;i<=n;i++){ 12 if(vis[i]==true)continue; 13 for(int j=i*2;j<=n;j+=i) 14 vis[j]=true; 15 size++; 16 } 17 18 end=clock(); 19 20 printf("%lf",(double)(end-start)/CLOCKS_PER_SEC); 21 return 0; 22 }
时间优化有了质的飞跃:

5.Leonhard Euler 的线性筛(欧拉筛)
欧拉,简直是数学史上的天神,他研究出的一笔画问题,欧拉回路问题,在信息学也有广泛的应用。
埃氏筛节省了许多时间,但用它筛去合数,必定有被重复筛去的部分,比如6,在筛2的倍数和筛3的倍数的过程中被重复筛去了。如果能让每个合数都只被标记一次,那么时间复杂度就可以降到O(n)了。
1 bool vis[N]; 2 int Prime[N],size; 3 4 int main(){ 5 clock_t start,end; 6 7 int n; 8 scanf("%d",&n); 9 start=clock(); 10 11 for(int i=2;i<=n;i++){ 12 if(!vis[i])Prime[++size]=i; 13 for(int j=1;j<=size;j++){ 14 if(i*Prime[j]>n)break; 15 vis[i*Prime[j]]=true; //能由两个数相乘而得,这一定是合数 16 if(!(i%Prime[j]))break; //如果Prime[j]是i的因数,不妨留到下一次筛 17 } 18 } 19 20 end=clock(); 21 22 printf("%lf",(double)(end-start)/CLOCKS_PER_SEC); 23 return 0; 24 }
为了让大家更好感受线性筛的筛法,我们修改修改代码:
1 bool vis[N]; 2 int Prime[N],size; 3 4 int main(){ 5 int n; 6 scanf("%d",&n); 7 for(int i=2;i<=n/2;i++){ 8 if(!vis[i])Prime[++size]=i; 9 printf("When i=%3d,the numbers given up are ",i); 10 for(int j=1;j<=size;j++){ 11 if(i*Prime[j]>n)break; 12 vis[i*Prime[j]]=true; 13 printf("%3d,",i*Prime[j]); 14 if(!(i%Prime[j]))break; 15 } 16 printf("\n"); 17 } 18 return 0; 19 }

所用时间:


浙公网安备 33010602011771号