1. 朴素埃筛+模板
可参考:[数论基础] 1. 输出素数(素数筛、线性筛、Miller-Rabbin 素性测试、巧妙解法)
思路:
- 从 2 到 n n n 将每一个数的所有倍数全部筛掉。这样筛过之后剩余的所有数都是质数。因为对于任意一个数 p p p,如果它没被筛掉,那么意味着从 2 到 p − 1 p-1 p−1 都没有它的因数,则为质数。
- 时间复杂度分析:
n
2
+
n
3
+
⋯
n
n
=
n
(
1
2
+
1
3
+
⋯
1
n
)
\frac n 2 + {\frac n 3}+ \cdots {\frac n n}=n(\frac1 2 + {\frac 1 3}+ \cdots {\frac 1 n})
2n+3n+⋯nn=n(21+31+⋯n1)。我们可以知道
1 2 + 1 3 + ⋯ 1 n \frac1 2 + {\frac 1 3}+ \cdots {\frac 1 n} 21+31+⋯n1 为调和级数,当 n n n 趋于正无穷时,其极限为 l n n + C lnn+C lnn+C,其中 C C C 为欧拉常数。那么朴素筛法的时间复杂度为 O ( n l n n ) O(nlnn) O(nlnn),也可记为 O ( n l o g n ) O(nlogn) O(nlogn),两者同阶。
这种写法是朴素埃筛,时间复杂度稍高,可以对其做优化。
朴素埃筛代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6+5;
int n;
int prime[N], cnt;
bool st[N];
void get_primes(int n) {
for (int i = 2; i <= n; ++i) {
if (!st[i]) {
prime[cnt ++] = n;
}
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
int main() {
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
2. 埃筛+模板
这才是真正的埃筛:只筛掉素数的倍数,已被筛过的数就不用再去筛掉它的倍数了。由算术基本定理可知每个数只用被素数筛掉即可。所以可以将 f o r for for 循环放到 i f if if 判断内部。
时间复杂度分析:由质数个数定理 1 到 n n n 当中有 n l n n \frac n {lnn} lnnn 个质数。所以本来调和级数需要计算 n n n 个数,但是现在只需要计算 l n n lnn lnn 个数,所以粗略估计时间复杂度为 O ( n l n n l n n ) = O ( n ) O(\frac {nlnn} {lnn})=O(n) O(lnnnlnn)=O(n)。 但是实际应当是 O ( n l n l n n ) O(nlnlnn) O(nlnlnn),可由数学证明。几乎是和 O ( n ) O(n) O(n) 一个级别的。
埃筛模板代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6+5;
int n;
int prime[N], cnt;
bool st[N];
void get_primes(int n) {
for (int i = 2; i <= n; ++i) {
if (!st[i]) {
prime[cnt ++] = n;
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
}
int main() {
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
3. 线性筛+模板
首先谈谈算法速度:
输入 n n n 大约在 1 0 6 10^6 106 时,两者时间复杂度差不多,线性筛稍快。
输入 n n n 大约在 1 0 7 10^7 107 时,线性筛比埃筛快一倍。
差别就在于 l n l n n lnlnn lnlnn。
埃筛的不足之处:
- 例如筛 12 这个数时,其可被因式分解为
12=2*3
,12=3*4
,所以当 2 和 3 作为质因子筛掉它们的倍数的时候,会将 12 重复筛掉两次。同理,18 这个数也是这样的。
然而,线性筛保证每一个合数只会被它的最小质因子筛掉:
- 从小到大枚举
prime[j]
,是存放已经得到的质数数组。 - 当循环内
if (i % prime[j] == 0) break;
成立时一定能够保证prime[j]
是i
的最小质因子,因为是从小到大枚举的。同时prime[j]
也一定是prime[j] * i
的最小质因子(想一下算术基本定理分解式),故可以筛掉prime[j] * i
这个数,且是由它的最小质因子筛掉的。然后直接break
掉,否则就会出现埃筛的重复情况。 - 当
i % prime[j] != 0
,那么意味着prime[j]
一定小于i
的所有质因子,那么prime[j]
就是prime[j] * i
这个数的最小质因子,所以用它的最小质因子将其筛掉。
所以算法保证了筛掉的每一个合数都是采用该数的最小质因子进行筛除,即,都是用 st[prime[j] * i] = true;
将其筛掉。
如果当 x
是一个合数,我们来讨论下它一定会被筛掉:
- 因为
x
为合数,则必存在最小质因子,不妨将其记为pj
,当i
枚举到x
之前一定会先枚举到i = x / pj
,那么此时就会在st[prime[j] * i] = true;
被筛掉,即st[pj * x / pj] = st[x] = true; 其中 pj 为 x 的最小质因子
。 故,用pj
这个x
的最小素因子,在x
之前的x/pj
这个数的时候,就会把x
提前筛掉。
那么至此证明,每个合数都会被筛掉,且每个合数只会被其最小质因子筛掉,所以每个合数只被筛了一次,那么这个算法就是线性的,时间复杂度为 O ( n ) O(n) O(n)。
细节:
在此内层的 for
循环判断条件没必要写成 j < cnt && prime[j] <= n / i
,分情况讨论:
- 当
i
为合数时,prime[j]
枚举到i
的最小质因子就会break
掉。 - 当
i
为质数时,代表没被提前筛过,已经将其加入到了primes
数组中,那么prime[j]
枚举到等于i
时也会停下来,此时两数取模变成 0,则break
。 - 所以无论如何一定会在
j>=cnt
前 停下来,所以不必加上那个限制条件 - 至于
prime[j] <= n / i
的话,可以理解为它是来控制范围的st[prime[j] * i] = true;
保证其不越界,同时筛掉i
的所有现存质因子的倍数。同时循环内部的if
判断保证了只被最小质因子所筛掉,不会重复筛。
当
i%primes[j]==0
时必须终止,假设不终止,那么i∗primes[j+1]
的最小质因子并不是primes[j+1]
,如12=2∗6
最小质因子为 2, 但12=2*2∗3
,primes[j+1]=3
,显然,最小质因子并不是 3,而是 2,所以找到了最小质因子后就直接将其跳出即可,12 会在i = 6
时,被 2 筛掉,而不是在i = 4
时被 3 筛掉。
prime
数组存放小于n
的所有素数,其个数为c
个。prime
数组中的素数是递增的,当i
能整除prime[j]
,那么i * prime[j + 1]
这个合数肯定被prime[j]
乘以某个数筛掉。因为i
中含有prime[j]
,prime[j]
比prime[j+1]
小,即i=k*prime[j]
,那i*prime[j+1]=(k*prime[j])*prime[j+1]=k’*prime[j]
,接下去的素数同理。所以不用筛下去了。因此,在满足i%prime[j]==0
这个条件之前以及第一次满足改条件时,prime[j]
必定是prime[j]*i
的最小因子。故不会对一个元素进行重复判断,将效率提升到 O ( n ) O(n) O(n)。
有点抽象,可移步 :
我以前的博文讲过相关知识点,可参考:[数论基础] 1. 输出素数(素数筛、线性筛、Miller-Rabbin 素性测试、巧妙解法)
线性筛模板代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6+5;
int n;
int prime[N], cnt;
bool st[N];
void get_primes(int n) {
for (int i = 2; i <= n; ++i) {
if (!st[i]) prime[cnt ++] = i;
for (int j = 0; prime[j] <= n / i; ++j) {
st[prime[j] * i] = true;
if (i % prime[j] == 0) break;
}
}
}
int main() {
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}