文章目录
1. 朴素埃筛+模板
可参考:
[数论基础] 1. 输出素数(素数筛、线性筛、Miller-Rabbin 素性测试、巧妙解法)
思路:
-
从 2 到
nn
n
将每一个数的所有倍数全部筛掉。这样筛过之后剩余的所有数都是质数。
因为对于任意一个数
pp
p
,如果它没被筛掉,那么意味着从 2 到
p−
1
p-1
p
−
1
都没有它的因数,则为质数。
-
时间复杂度分析:
n2
+
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})
2
n
+
3
n
+
⋯
n
n
=
n
(
2
1
+
3
1
+
⋯
n
1
)
。我们可以知道
12
+
1
3
+
⋯
1
n
\frac1 2 + {\frac 1 3}+ \cdots {\frac 1 n}
2
1
+
3
1
+
⋯
n
1
为
调和级数
,当
nn
n
趋于正无穷时,其极限为
ln
n
+
C
lnn+C
l
n
n
+
C
,其中
CC
C
为欧拉常数。那么
朴素筛法的时间复杂度为
O(
n
l
n
n
)
O(nlnn)
O
(
n
l
n
n
)
,也可记为
O(
n
l
o
g
n
)
O(nlogn)
O
(
n
l
o
g
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;
}
2. 埃筛+模板
这才是真正的埃筛
:只筛掉素数的倍数,已被筛过的数就不用再去筛掉它的倍数了。由算术基本定理可知每个数只用被素数筛掉即可。所以可以将
f
o
r
for
f
o
r
循环放到
i
f
if
i
f
判断内部。
时间复杂度分析
:由质数个数定理 1 到
n
n
n
当中有
n
l
n
n
\frac n {lnn}
l
n
n
n
个质数。所以本来调和级数需要计算
n
n
n
个数,但是现在只需要计算
l
n
n
lnn
l
n
n
个数,所以
粗略估计
时间复杂度为
O
(
n
l
n
n
l
n
n
)
=
O
(
n
)
O(\frac {nlnn} {lnn})=O(n)
O
(
l
n
n
n
l
n
n
)
=
O
(
n
)
。 但是
实际应当
是
O
(
n
l
n
l
n
n
)
O(nlnlnn)
O
(
n
l
n
l
n
n
)
,可由数学证明。几乎是和
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
1
0
6
时,
两者时间复杂度差不多,线性筛稍快。
输入
n
n
n
大约在
1
0
7
10^7
1
0
7
时,
线性筛比埃筛快一倍。
差别就在于
l
n
l
n
n
lnlnn
l
n
l
n
n
。
埃筛的不足之处:
-
例如筛 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;
}