何为线性筛法,顾名思义,就是在线性时间内(也就是O(n))用筛选的方法把素数找出来的一种算法,没用过线性筛素数法的人可能会奇怪,用遍历取余判定素数不是也是线性时间的吗,没错,但是确切的说线性筛法并不是判定素数的,而是在线性时间内求出一个素数表,需要判定是否是素数的时候只要看该数是否在表内就可以瞬间知道是不是素数。
比如想求10000以内的素数,定义表int a[10000],进行线性筛选后,a[n]的值就代表n是不是素数,a[n]如果是1,就代表n是素数,a[n]如果是0,就代表n不是素数,这就是查表。再判定其他的素数也是一样,不用再做任何计算。
而如果用遍历取余,那么每判定一个数都要从头开始再遍历一遍,而线性筛法只在开始一次性运算完,以后只要查表即可,查表通常只需要1条语句。所以如果你的程序从始至终只需要判定那么几次素数那么用遍历取余即可,但是如果需要多次判定素数,而且这个数还不是很小的话,那么线性筛法就会体现出巨大的优越性来。
线性筛法的核心原理就是一句话: 每个合数必有一个最大因子(不包括它本身) ,用这个因子把合数筛掉,还有另一种说法(每个合数必有一个最小素因子,用这个因子筛掉合数,其实都一样,但是我觉得这种方法不太容易说明,这种方法我会在最后给出简略说明)。这个很容易证明:这个小学就知道合数一定有因子,既然是几个数,就一定有最大的一个。最大因子是唯一的,所以合数只会被它自己唯一的因子筛掉一次,把所有合数筛掉后剩下的就全是素数了。
先假设一个数i,一个合数t,i是t最大的因数,t显然可能并不唯一(例如30和45的最大因数都是15)。那么如何通过i知道t呢,t必然等于i乘以一个比i小的素数。先来说这个数为什么一定要比i小,这很显然,如果是i乘上一个比它大的素数,那么i显然不能是t最大的因子。再来说为什么要是素数,因为如果乘上一个合数,我们知道合数一定可以被分解成几个素数相乘的结果,如果乘上的这个合数x=p1p2……,那么t = i * x = i * p1 * p2……很显然p1* i也是一个因数,而且大于i。所以必须乘上一个素数。
比i小的素数一定有不少,那么该乘哪一个呢,既然t不唯一,那么是不是都乘一遍呢?很显然不行,虽然t不唯一,但全乘一遍很显然筛掉的数的数量远远超过合数的数量。我们先给出结论:
任意一个数i = p1p2……*pn,p1、p2、……pn都是素数,p1是其中最小的素数,
设T 为i * M的积(显然T就成了一个合数),也就是T = i * M,(M是素数,并且M<=p1),那么T的最大的因数就是i。
是的,乘上的数要小于等于i最小的质因数。
为什么呢?我来证明这一点。
假设i可以表示为素数乘积:i = p1p2…pn,其中i最小的素因数是p1,设M是素数,并且大于i最小的素因数p1。设iM=y,显然同时y=M * p1p2*……pn因为M>p1,显然
Mp2……pn要大于i=p1p2……pn,Mp2……pn又很显然是y的一个因数,那么y的最大因数就不是i。由此说明了了上面给出的结论。
本文给出的证明方法并不是很严格,很不严密,但是本文只是想解释线性筛素数的算法,并不是想严格证明,如果想看严格证明请看数论中的证明。另上面提到的线性筛素数的另一种说法,其实到这里读者应该差不多明白了,任何一个合数都可分解为一个素数和另一个数(不一定是素数还是合数)的乘积。我们既然找到了这个合数最大的因数,那么根据上面结论里另一个乘上的素数必然就是他的最小素因数。另一种说法只不过是换一个说法罢了。
最后我们就可以得出结论:对于每一个数i,乘上小于等于i的最小素因数的素数,就得到以i为最大因数的合数。设有一个数t,只要将所有以比t小的数为最大因数的合数筛去,那么比t小的数里剩下的就只有素数了。这就是线性筛法求素数的方法。
typedef long long ll;
const int N = 1e5 + 10;
int primes[N], cnt;
bool st[N];
void init()
{
for(int i = 2; i < N; i++)
{
if(!st[i]) primes[cnt ++] = i;
for(int j = 0; primes[j] * i < N; j ++)
{
st[primes[j] * i] = 1;
if(i % primes[j] == 0) break;
}
}
}