质数检验
引题:
质数指约数仅为1及其本身的自然数。比如8个最小的质数为2,3,5,7,11,13,17,19。注意,1不是质数。
请编写一个程序,输入n个整数,输出其中质数的个数。
输入:第一行输入n。接下来1行给出n个整数。
输出:输出质数的个数,占1行。
限制:
1
<
=
b
<
=
10000
,
2
<
=
给
出
的
整
数
<
=
1
0
8
1<=b<=10000,2<=给出的整数<=10^8
1<=b<=10000,2<=给出的整数<=108
输入示例:
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),算法整体的复杂度与 x i ( i = 1 , 2 , … , n ) x_i(i=1,2,…,n) xi(i=1,2,…,n)的总和成正比,显然无法在限制时间内输出答案。我们需要考虑一个更高效的方法。
2. 初学者优化算法
- 除2以外所有的偶数都不是质数,这样就能将复杂度减少一半。
- 在检查x时,由于x不可能被大于 x 2 \frac{x}{2} 2x的整数整除,这就又减少了一半复杂度。
但这些小技巧并不能撼动该算法复杂度为O(x)的本质。
3. 性质:若n为合数,则必有质数p|n,且 p < = n p<=\sqrt{n} p<=n
在检验质数的时候,我们可以利用“合数x拥有满足
p
<
=
x
p<=\sqrt{x}
p<=x的质因数p”这一性质。
举个例子,检验31是否为质数时,只需要看31能否被2到6的整数整除即可。如果7到30中存在能整除31的整数,那么2到6中必然也存在能整除31的整数,所以检查大于6的整数只是浪费资源。
利用这一性质,我们可以将检验范围从2到x-1缩小至2到
x
\sqrt{x}
x,算法的复杂度也就改良到了O(
x
\sqrt{x}
x)。比如x=1000000时,
x
\sqrt{x}
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)可以快速列举出给定范围内的所有质数,这个算法如下步骤生成质数表。
埃拉托色尼筛选法:
- 列举大于等于2的整数。
- 留下最小的整数2,删除所有2的倍数。
- 在剩下的整数中留下最小的3,删除所有3的倍数。
- 在剩下的整数中留下最小的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 | 5 | 7 | 9 | |||||
---|---|---|---|---|---|---|---|---|---|
11 | 13 | 15 | 17 | 19 | |||||
21 | 23 | 25 | 27 | 29 | |||||
31 | 33 | 35 | 37 | 39 | |||||
41 | 43 | 45 | 47 | 49 | |||||
51 | 53 | 55 | 57 | 59 |
第二轮:
2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | ||
---|---|---|---|---|---|---|---|---|---|
11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | |
22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | ||
31 | 32 | 34 | 35 | 36 | 37 | 38 | 40 | ||
41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | |
52 | 53 | 54 | 55 | 56 | 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 | 26 | 27 | 28 | 29 | 30 | |
31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 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 | 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)处。
这时,我们会遇到两种情况:
- 如果 i 是素数的话,那简单,一个大的素数 i 乘以一个不大于 i 的素数ans[j],这样筛除的数跟之前的是不会重复的。筛出的数都是 N = p 1 ∗ p 2 N=p_1*p_2 N=p1∗p2 的形式, p 1 p_1 p1、 p 2 p_2 p2之间不相等。
- 如果 i 是合数的话,此时 i 可以表示成递增素数相乘 i = p 1 ∗ p 2 ∗ . . . ∗ p n i=p_1*p_2*...*p_n i=p1∗p2∗...∗pn , p i p_i pi都是素数(2<=i<=n), p i < = p j ( i < = j ) p_i<=p_j ( i<=j ) pi<=pj(i<=j),其中 p 1 p_1 p1是最小的素数。而且满足条件的 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;//保证每个合数只会被它的最小质因数筛去
}
}
}