埃(埃拉托斯特尼)筛
思想
质数的倍数一定不是质数。这个人人都会,超级简单。
步骤
找1-n范围内的质数。
0、首先,2是质数,i = 2
1、如果i
是质数(标记为0),令j = i
,转到第2步;否则直接转到第3步
2、i * j
一定不是质数,将其标记为1。j++
。重复此步骤,直到i * j > n
3、i++
,如果i * i <= n
,转到第2步,否则转到第4步
4、检查所有1-n
范围内的数,标记为0的数就是质数。
埃拉托斯特尼筛法的时间复杂度是 O ( n l o g ( l o g n ) ) O(nlog(logn)) O(nlog(logn))。证明需要高深的知识,这里不讲。不过有一点可能有帮助,就是高斯定理:1-n中质数的个数大约为 n / l n n n/ln\ n n/ln n。
int prime[1000000], cnt;
short flag[10000000];
void era(int n) {
int i, j;
for (i = 2; i * i <= n; i++) {
if (flag[i]) continue; // 合数的倍数一定被它的某个质因子筛过,无需重复
prime[cnt++] = i;
for (j = i; j * i <= n; j++) flag[i * j]++; // flag数组记录被标记的次数
}
for (; i <= n; i++) {
if (!flag[i]) prime[cnt++] = i;
}
}
线性筛
思想
我们要想办法把这个
l
o
g
(
l
o
g
n
)
log(logn)
log(logn)去掉才完美。观察埃拉托斯特尼筛法的过程,可以看到有重复筛的情况,比如,2*6=12
,被2的倍数筛一次;3*4=12
,又被3的倍数筛一次。这种重复导致埃筛的时间不是严格线性的。可以试着输出flag
数组,发现多数合数并不仅被标记一次。可以证明,n的标记的次数就等于它的不同的质因子个数。
显然,每个合数都能分解为质数的乘积的形式(分解质因子),这些质因子中必然有一个最小的。我们线性筛的目的是让每个合数只被筛掉一次,那么一个最简单的方法就是让每个合数仅被最小质因子筛掉。
如何做到呢?我们先分析一下一个合数的分解: t = p ∗ k t=p*k t=p∗k。如果 p p p是最小质因子,那么首先就可以得出一个大前提: p < t p<t p<t。然后再分两种情况讨论:
1、k是质数,显然 k ≥ p k≥p k≥p
2、k是合数,将其质因子分解为 k = p 1 r 1 p 2 r 2 . . . p n r n k=p_1^{r_1}p_2^{r_2}...p_n^{r_n} k=p1r1p2r2...pnrn。那么对于任意的 i = 1 , 2 , . . . , n i=1,2,...,n i=1,2,...,n,都有 p i ≥ p p_i≥p pi≥p。
上面这个结论非常重要!非常重要!请务必吃透!
线性筛的核心思想是基于 k k k标记 t = p ∗ k t=p*k t=p∗k;而埃筛是基于 p p p标记 t = p ∗ k t=p*k t=p∗k。共同点就是都满足 p ≤ k p≤k p≤k。
来看埃筛的核心代码:
for (i = 2; i * i <= n; i++) { // i 就是上面所说的 p
if (flag[i]) continue;
prime[cnt++] = i;
for (j = i; j * i <= n; j++) flag[i * j]++; // j 就是上面所说的 k
}
p是外层循环,k是内层循环。
下面我们改换一下循环策略。埃筛的策略是,外层循环找质数,内层循环找质数的倍数。线性筛的策略是,外层循环i
遍历所有的数(无论它是质数还是合数),内层循环j
是基于目前已找到的质数的,用来标记i * j
,也就是i
的质数倍。
由此,可得出线性筛的核心代码:
for (i = 2; i <= n; i++) { // i 就是上面所说的 k
if (!flag[i]) prime[cnt++] = i;
for (j = 0; j < cnt && i * prime[j] <= n; j++)
flag[i * prime[j]]++; // prime[j] 就是上面所说的 p
}
但是别高兴太早!这实现了我们线性筛的目的了吗?并没有,我们只是把埃筛的循环顺序改换了一下而已,实际上,我们标记质数的时候依然有重复,而且重复次数和埃筛一毛一样!(稍微思考一下就懂了)
我们刚才是不是说过最小质因子的概念啊?一定时刻记住,线性筛的核心思想就是每个数都只用最小质因子标记一次。我们忽略了一点,刚才的代码中,prime[j]
是否是i * prime[j]
的最小质因子。如果不是最小质因子,那就表明i * prime[j]
已经被更小的质因子标记过!再次标记就会重复了。下面我们就来解决这个问题。
真正的线性筛
刚才的基本结论说过,
t
=
p
∗
k
t=p*k
t=p∗k,如果p是最小质因子,那么k的所有质因子都大于等于p。上面的代码中,对所有i * prime[j]
都无脑标记了一遍。但如果i % prime[j] == 0
,那这样标记显然无意义了,因为我们标记的是**i
的倍数**,此时prime[j]
可以整除i
,i
的倍数也一定是prime[j]
的倍数,也就是已经被prime[j]
标记过了。同时可以得知,prime[j + 1]
及后面的其它质数一定不是i * prime[j + 1]
的最小质因子,因为i * prime[j + 1]
、i * prime[j + 2]
等等,它们一定能被更小的质因子prime[j]
整除。
所以说核心代码很简单,只需要在上面的代码加上一句判断整除:
for (i = 2; i <= n; i++) { // i 就是上面所说的 k
if (!flag[i]) prime[cnt++] = i;
for (j = 0; j < cnt && i * prime[j] <= n; j++) {
flag[i * prime[j]]++; // prime[j] 就是上面所说的 p
if (i % prime[j] == 0) break; // 后面的质数已经不是最小质因子了
}
}
这样就一定保证了每个合数只被最小质因子筛一次。所以根据均摊分析,时间复杂度降到了真正的 O ( n ) O(n) O(n)。下面是完整代码:
int prime[1000000], cnt;
short flag[10000000];
void linear(int n) {
int i, j;
for (i = 2; i <= n; i++) {
if (!flag[i]) prime[cnt++] = i;
for (j = 0; j < cnt && i * prime[j] <= n; j++) {
flag[i * prime[j]]++;
if (i % prime[j] == 0) break;
}
}
}
再次尝试输出flag数组,发现所有的数标记次数均为0或1。
时间测试
来比较一下两种筛法的性能。为了放大效果,必须用较大的数来测试:
n | 埃筛运行时间(毫秒) | 线性筛运行时间(毫秒) |
---|---|---|
1e6 | 12 | 9 |
5e6 | 85 | 54 |
1e7 | 179 | 123 |
2e7 | 371 | 246 |
5e7 | 996 | 564 |
1e8 | 2143 | 1173 |
2e8 | 4421 | 2278 |
3e8 | 6564 | 3432 |
4e8 | 8990 | 4651 |
可以看到线性筛以接近2倍的速度吊打埃筛。
对“线性”的讨论
我们所说的线性筛,其时间复杂度是针对输入的n值来说的。实际上,在时间复杂度理论中,时间复杂度是针对输入规模(直观来说就是输入文件大小)的。整数n的输入规模就是 k = l o g 10 n k=log_{10}n k=log10n,而 O ( n ) O(n) O(n)显然不是k的多项式复杂度,而是指数复杂度。这叫做伪多项式时间(此问题可以称作伪线性时间)。
已经证明,寻找1-n的所有质数是一个NP-hard问题。