4.质数判定和质数筛法(埃拉托色尼筛选法,线性筛法/欧拉筛法)

质数检验

引题
质数指约数仅为1及其本身的自然数。比如8个最小的质数为2,3,5,7,11,13,17,19。注意,1不是质数。
请编写一个程序,输入n个整数,输出其中质数的个数。

输入:第一行输入n。接下来1行给出n个整数。
输出:输出质数的个数,占1行。
限制1<=b<=100002<=<=1081<=b<=10000,2<=给出的整数<=10^8
输入示例
6
2 3 4 5 6 7
输出实例
4

1. 初学者的简单算法

检验整数x是否为质数:检查整数x能否被2到x-1的整数整除。

bool isPrime( int x )
{
	if( x<=1 ) return false;
	for(int i=2;i<x;++i)
		if( x%i==0 )
			return false;
	return true;
}

可以得知,上述算法对于单一数据其复杂度为O(x),算法整体的复杂度与xi(i=1,2,,n)x_i(i=1,2,…,n)的总和成正比,显然无法在限制时间内输出答案。我们需要考虑一个更高效的方法。

2. 初学者优化算法
  1. 除2以外所有的偶数都不是质数,这样就能将复杂度减少一半。
  2. 在检查x时,由于x不可能被大于x2\frac{x}{2}的整数整除,这就又减少了一半复杂度。

但这些小技巧并不能撼动该算法复杂度为O(x)的本质。

3. 性质:若n为合数,则必有质数p|n,且p&lt;=np&lt;=\sqrt{n}

在检验质数的时候,我们可以利用“合数x拥有满足p&lt;=xp&lt;=\sqrt{x}的质因数p”这一性质。
举个例子,检验31是否为质数时,只需要看31能否被2到6的整数整除即可。如果7到30中存在能整除31的整数,那么2到6中必然也存在能整除31的整数,所以检查大于6的整数只是浪费资源。
利用这一性质,我们可以将检验范围从2到x-1缩小至2到x\sqrt{x},算法的复杂度也就改良到了O(x\sqrt{x})。比如x=1000000时,x\sqrt{x}=1000,此时该算法快了1000倍。

bool isprime(int x)
{
	if(x==2) return true;
	if( (x<2) || !(x&1) ) return false;//x<2或x为偶数
	int i = 3;
	while( i<=sqrt(x) )
	{
		if( x%i==0 ) return false;
		i += 2;
	}
	return true;
}
4. 埃拉托色尼筛选法

有些时候,除了检验给定整数x是否为质数的函数之外,如果能事先准备出质数数列或质数表,就可以帮助我们更有效地求解质数的相关问题。
埃拉托色尼筛选法(The Sieve of Eratosthenes)可以快速列举出给定范围内的所有质数,这个算法如下步骤生成质数表。
埃拉托色尼筛选法

  1. 列举大于等于2的整数。
  2. 留下最小的整数2,删除所有2的倍数。
  3. 在剩下的整数中留下最小的3,删除所有3的倍数。
  4. 在剩下的整数中留下最小的5,删除所有5的倍数。
  5. 以下同理,留下仍未被删除的最小整数,删除该整数的倍数,一直循环到结束。

以最小的4个质数为例,其求解过程如图。
原始表:

2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60

第一轮:

2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60

第二轮:

2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60

第三轮:

2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60

第四轮:

2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60

埃拉托色尼筛选法核心代码:

#define MAX 1000000
bool isprime[MAX];
//bool型数组isprime表示质数表,
//isprime[x]为true表示x是质数,为false表示x是合数。
void eratos(int n)
{
	int i,j;
	for(i=0;i<=n;++i)//列举整数作为候选的质数
		isprime[i] = true;
	isprime[0] = isprime[1] = false;//0和1不是质数,所以删除它们
	for(i=2;i<=sqrt(n);++i)//留下i,删除i的倍数
		if( isprime[i] )
		{
			j = i+i;
			while( j<=n )
			{
				isprime[j] = false;
				j = j+i;
			}
		}
}

埃拉托色尼筛选法需要占用一部分内存空间(与待检验整数的最大值N成正比),但其复杂度只有O(N log log N)。

其实可以再优化一下代码:

#define MAX 1000000
bool isprime[MAX];
//bool型数组isprime表示质数表,
//isprime[x]为true表示x是质数,为false表示x是合数。
void eratos(int n)
{
	int i,j;
	for(i=0;i<=n;++i)//列举整数作为候选的质数
		isprime[i] = true;
	isprime[0] = isprime[1] = false;//0和1不是质数,所以删除它们
	for(i=2;i<=sqrt(n);++i)//留下i,删除i的倍数
		if( isprime[i] )
		{
			j = i*i;	//这边其实从i*i开始就可以了
			while( j<=n )
			{
				isprime[j] = false;
				j = j+i;
			}
		}
}

但其实你自己手动模拟一遍就会发现,埃拉托色尼筛选法不足之处也很明显,很多数被处理了不止1遍,比如6,在素数为2的时候处理1次,为3时候又标记一次,因此又造成了比较大的不必要处理。因此,下面引出线性筛法(欧拉筛法)求质数。

5. 线性筛法(欧拉筛法)

思路:
筛的过程中要保证两点:
1、合数一定被删掉了
2、每个数都没有被重复地删掉

那代码如何实现呢?(我们结合下面的代码分析)
首先,明确一个条件,任何合数都能表示成一系列素数的积。
其次,不管 i 是否是素数,都会执行到第18行——for(int j=0;j<tot;++j)处。
这时,我们会遇到两种情况:

  1. 如果 i 是素数的话,那简单,一个大的素数 i 乘以一个不大于 i 的素数ans[j],这样筛除的数跟之前的是不会重复的。筛出的数都是 N=p1p2N=p_1*p_2 的形式, p1p_1p2p_2之间不相等。
  2. 如果 i 是合数的话,此时 i 可以表示成递增素数相乘 i=p1p2...pni=p_1*p_2*...*p_n , pip_i都是素数(2<=i<=n), pi&lt;=pj(i&lt;=j)p_i&lt;=p_j ( i&lt;=j ),其中p1p_1是最小的素数。而且满足条件的 i 有且只有一个,所以不会重复删除。

再次,根据第23行——if( i%ans[j] == 0 )处的定义,当最小的素数p1等于ans[j]的时候,筛除就终止了,也就是说,只能筛出不大于最小素数p1的质数的i倍的数。即保证每个合数只会被它的最小质因数筛去,从而不重复筛选。

线性筛法核心代码:
#define MAX 1000000
int ans[MAX];//记录哪些数字是素数,即ans[MAX]是一个素数集合
bool valid[MAX];
//数组valid[i]记录i是否为素数。初始所有的valid[i]为true。
//线性筛选完之后valid[i]=ture的就是素数。
void LinearPrime(int n)
{
	int tot = 0;
	memset(valid,true,sizeof(valid));//初始所有的valid[i]为true
	for(int i=2;i<=n;++i)//对于2到n中的每一个i
	{
		if( valid[i] )//如果i是素数
		{
			ans[tot] = i;//那么就将i这个素数加入素数集合ans[MAX]中
			tot++;
		}
		for(int j=0;j<tot;++j)//对于当前素数集合中每一个元素ans[j]
		{
			if( i*ans[j]>n ) break;//若i*ans[j]>n,结束循环
			//(因为确定素数的倍数超过讨论范围,没必要进行下一步了)
			valid[i*ans[j]] = false;//将确定素数的倍数记录为合数
			if( i%ans[j]==0 ) break;//保证每个合数只会被它的最小质因数筛去
		}
	}
}
posted @ 2019-03-04 11:00  Sherry_Yue  阅读(860)  评论(0编辑  收藏  举报