算法不是记模板,而是启发思维
判断一个数是否是素数,首先我们明晰素数的概念。
- 素数:素数一般指质数。质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。
暴力
由此我们可以从2遍历到x-1,判断x能否被整除,写出最简单暴力的IsPrime(int x)
bool IsPrime(int x)
{
if(x==1)return false;
bool flag=true;//标记是否是素数
for(int i=2;i<x;++i)
{
if(x%i==0)
{
flag=false;
break;
}
}
return flag;
}
显然,单次查询的复杂度为O(n)。
但如果x>1e12,一般都会超时
暴力优化
接下来,我们对其进行一次优化
如果x能被2到x-1其中的一个数k整除,定义k’ =x/k,x也一定能被k’整除,x在产生一个因数的同时会产生另一个因数。
并且我们发现一个神奇的现象,就是k和k’中一个<=sqrt(x)一个>=sqrt(x),成对出现的因数分别分布在<=sqrt(x)的一侧和>=sqrt(x)的一侧,那么也许我们就不需要从2遍历到x-1来查找x的因数,只需要从2遍历到sqrt(x)就能x判断是否是素数
给出代码
bool IsPrime(int x)
{
if(x==1)return false;
bool flag=true;
int end=sqrt(x);
for(int i=2;i<=end;++i)
{
if(x%i==0)
{
flag=false;
break;
}
}
return flag;
}
这样的时间复杂度为O( x \sqrt{x} x) 即使x在1e12附近,也能快速得出
上面我们讨论的是单次查询的情况,如果是多次判断x,判断T次,(1<=x<=1e6,t<=1e6)
时间复杂度O(
x
\sqrt{x}
x*T),时间也会比较慢,很容易超时
由此,我们请出今天的主角——素数筛
埃氏筛法
查询的次数较大时,我们通常采用数组预处理存储状态的方法来防止时间过大,这样预处理后,每次查询可以直接得出答案,不必重新判断。
用一个数组Prime[]来存储是否是素数,是素数标记为0,不是则标记为1。并且与上面不同的是,我们在程序开始前对此数组进行预处理。
首先对把2作为因数,累加上去把4,6,8…这些以2为因数的数为下标的值变为1,再是以3作为因数6,9,12,变为1。最后以x-1为因数,至此,所有合数都已经被设为1.
代码
int Prime[(int)1e6+5];
void pre_prime(int x)
{
Prime[1]=1;
for(int i=2;i<x;i++)
{
if(Prime[i]==0)
for(int j=i+i;j<=x;j+=i)
Prime[j]=1;
}
}
我们还可以对其进行优化,我们发现在遍历的过程中有些是重复的,如i=3时,我们把6,9,12变为1,但在之前6已经被2遍历,Prime[6]已经是1了。
按照我们之前的写法,是对于一个数来说他的因数总是成对出现的,所有合数都会被遍历两次,如在处理因数i的时候(i-1)* i,在之前肯定被i-1的因数遍历了,但如果我们把j的初始值设为i *i,那么就可以避免这个重复。
int Prime[(int)1e6+5];
void pre_prime(int x)
{
Prime[1]=1;
for(int i=2;i<x;i++)
{
if(Prime[i]==0)
for(int j=i*i;j<=x;j+=i)
Prime[j]=1;
}
}
然后我们可以再对其做出一个小优化,大于sqrt(x)的i,第二个循环一定不进行因为要满足j(i* i)<=x
int Prime[(int)1e6+5];
void pre_prime(int x)
{
Prime[1]=1;
int end=sqrt(x);
for(int i=2;i<=end;i++)
{
if(Prime[i]==0)
for(int j=i*i;j<=x;j+=i)
Prime[j]=1;
}
}
下面给出埃氏筛法的gif演示(图片源于网络)
但我们可以发现,在这之中还是有重复的遍历,如120被2、3、5重复遍历
接下来介绍更加神奇的欧拉筛法