算法-生成质数

前言

获取质数是常见的需求,假如我们想获取 2n2 \sim n(假设 nNn \le NNN 为已知的常数)的所有质数,应该怎么办呢?

两种选择

检查质数

首先不难想到,如果我们想达到这个目的,只需要检查这个范围内的每个数是不是质数就行了。

质数的定义是,除 11 和它本身以外没有其它因数的数,如果我们这么选择,那么在检查一个数 xx 时,需要至少判断 2x2 \sim \sqrt x 中的每个数是否都不能被 xx 整除(之所以是 x\sqrt x 是因为:如果 y>xy \gt \sqrt x 能被 xx整除,那么 xy<x\dfrac{x}{y} \lt \sqrt x 也能被 xx 整除,所以检查小于 x\sqrt x的部分足矣)。

那么对于 2n2 \sim n 整体,就要检查 n\displaystyle \sum \sqrt n 次。估算时间复杂度约为 O(nn)O(n \cdot \sqrt n)

筛去合数

有没有更好的做法呢?

已知合数的定义和质数相反,只要存在其它因子就算合数。

那么如果检查每个数是否为合数,就比检查质数方便得多,因为只需要找到一个反例即可。

怎么找合数呢?

埃拉托斯特尼筛法(埃筛)

初步想法

首先对于每个数,将它乘以 xNx \in \mathbb N 倍得到的一定是合数。

所以我们可以先从 22 开始,得到 4,6,8,\textcolor{red} 4, \textcolor{blue} 6, 8, \cdots,再从 33 开始,得到 6,9,12,\textcolor{blue} 6, 9, 12, \cdots,由于 4\textcolor{red} 4 被筛去,所以跳到 510,15,20,5 \rightarrow 10, 15, 20, \cdots,由于 6\textcolor{blue} 6 被筛去,所以跳到 77\cdots

C++为例,代码如下(假设长度为 NN 的数组不会导致过高的内存占用,nnint范围内):

1
2
3
4
5
6
7
8
9
10
11
12
13
bool is_prime[N + 1];

void gen_primes(int n) {
for(int i = 2; i <= n; i++) {
is_prime[i] = true;
}
for(int i = 2; i <= n; i++) {
if(!is_prime[i]) continue;
for(int j = 2; j <= n / i; j++) {
is_prime[i * j] = false;
}
}
}

两重优化

你肯定注意到这种方式有一定缺陷,比如 6\textcolor{blue} 6 同时被 2×32 \times 33×23 \times 2 筛去了,所以可以限制 jij \le i,这样就不会出现 3×23 \times 2 这种情况了。

另外,i>ni \gt \sqrt n 时,i×ji \times j 不可能 $ \lt n$,所以只需考虑 i[2,n]i \in \left[2,\sqrt n \right] 即可。

恭喜你发现了埃筛!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool is_prime[N + 1];
vector<int> primes;

void gen_primes(int n) {
for(int i = 2; i <= n; i++) {
is_prime[i] = true;
}

const int limit = sqrt(n);
for(int i = 2; i <= limit; i++) {
if(!is_prime[i]) continue;
for(int j = i; j <= n / i; j++) {
is_prime[i * j] = false;
}
}

for(int i = 2; i <= n; i++) {
if(is_prime[i]) primes.push_back(i);
}
}

计算时间复杂度

对于质数 22,我们筛去了 n2\dfrac n 2 个合数,33 筛去了 n3\dfrac n 3 个合数,\cdots

那么总操作次数就是 $\displaystyle \sum_p \dfrac n p $,由于 1nlogn\displaystyle \sum \dfrac 1 n \approx \log n,质数的数量也是 $ \dfrac 1 \log$ 级别,所以总时间复杂度为 O(nloglogn)O(n \log \log n)(注:严谨证明需借助积分,此处仅作直观理解故略去)。

欧拉筛(线性筛)

经过优化之后,我们还是可以发现一些问题。

比如 30=2×15=5×630 = 2 \times 15 = 5 \times 6,就会被筛去两次。

有没有什么办法,能让每个合数都只被筛去一次呢?

筛合数的办法

我们可以将一个合数描述为:最小质因子 ×\times 其它。

这样就可以对所有的 i[2,n]i \in \left[2,n\right] 乘上质数 pp,得到所有合数。

提前终止

首先,类似于刚才提到的 jij \le i,你想到了可以限制 pip \le i,防止 15153×53 \times 55×35 \times 3 同时筛去。

那么现在产生了一个新问题,12=6×2=4×312=6 \times 2 = 4 \times 3,要怎么避免它同时被 4466 筛去呢?

之所以会有这种情况,是因为 44 有比 33 更小的质因数:22,所以才会产生 4×3=2×(2×3)=2×64 \times 3 = 2 \times (2 \times 3) = 2 \times 6 的情况。

那么既然对于任意一个数 xx,都有 4×x=2×2x4 \times x = 2 \times 2x,那么 4x4x 这个数就不需要被 44xx 筛去了。

再比如,18=6×3=9×218 = 6 \times 3 = 9 \times 2,根据刚才的推理,因为 66 有更小的质因数 22,所以 1818 不该被 66 筛去。

所以,如果 ppii 的因数,那么就应该停止 pp 大的质数ii 相乘(因为 ii 有更小的质因数 pp)。

综上,pp 的终止条件有两个(至少一个成立就终止):

  • p>ip \gt i
  • imodp=0i \mod p = 0

由于当 i=pi = p 时,imodp=0i \mod p = 0,所以只需考虑第二个条件即可。

以下为C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool is_prime[N + 1];
vector<int> primes;

void gen_primes(int n) {
for(int i = 2; i <= n; i++) {
is_prime[i] = true;
}
for(int i = 2; i <= n; i++) {
if(is_prime[i])
primes.push_back(i);
for(int p : primes) {
if(1ll * i * p > n) break;
is_prime[i * p] = false;
if(i % p == 0) break;
}
}
}

恭喜,你发明了欧拉筛!

这种筛法对每个合数都只筛去了一次,所以时间复杂度是 O(n)O(n)

总结

大多数时候,使用埃筛就足够了,如果要追求极致速度,可以选择线性筛。

需要注意,线性筛筛去合数的过程并不是按大小筛的,下面给出一部分筛合数的过程:

合数 ii pp
44 22 22
66 33 22
99 33 33
88 44 22
1010 55 22
1515 55 33
2525 55 55
1212 66 22
1414 77 22
2121 77 33
1616 88 22
1818 99 22
2727 99 33
2020 1010 22
2222 1111 22
2424 1212 22
2626 1313 22
2828 1414 22
3030 1515 22
\cdots \cdots \cdots