一个证明
在讲筛法前,先证明一个东西:
对于一个合数 x ,一定存在一个质数 p ≤ x 且 p ∣ x 。 对于一个合数 x,一定存在一个质数 p \leq \sqrt{x} 且 p | x。 对于一个合数x,一定存在一个质数p≤x且p∣x。
-
先把结论考虑简单一点:合数 x x x一定存在一个小于等于 x \sqrt{x} x的因数。
显然一个合数 x x x可以被一对非 1 1 1非 x x x的整数 p , q p,q p,q表示为 x = p q x=pq x=pq,
则其中至少有一个整数小于 x x x,
因为若 p , q > x p,q > \sqrt{x} p,q>x,那么 p q > x pq > x pq>x。
-
再进一步考虑这个结论:合数 x x x一定存在一个小于等于 x \sqrt{x} x的质因数。
我们先假设每个合数不存在非 1 1 1质因子,
对于合数 x x x,它一定有一个的小于等于 x \sqrt{x} x的非 1 1 1因数 p p p,
而已经假设了每个合数不存在质因子,所以 p p p为合数,
那么对于合数 p p p, 它一定又有一个小于等于 p \sqrt{p} p的非 1 1 1合数因子 p 1 p_1 p1,
则 p 1 p_1 p1也为 a a a的因子,
同上可得: p 1 p_1 p1也一定有一个小于等于 p 1 \sqrt{p_1} p1的非 1 1 1合数因子 p 2 p_2 p2,
p 2 p_2 p2也为 a a a的因子,
…
如此循环往复可以做到 a a a的一个因子 p n ≤ . . . p_n\leq\sqrt{\sqrt{\sqrt{\sqrt{...}}}} pn≤...,
显然不等号右边的式子可以轻而易举地做到【大于 1 且 小于 2】,由于 ( 1 , 2 ) (1, 2) (1,2)区间内不存在合数,所以假设不成立。
筛法
筛法是对质数和合数进行分类的工具。
假如将所有整数放进一个筛子,那么这个筛子会筛掉合数,留下质数。
一个正确的筛法要同时满足两点:
- 留下的都是质数
- 筛去的都是合数和 01 01 01
本章介绍三种筛法及其优化和正确性证明
普通筛
普通筛是针对某一个单独的整数判断它是否为质数。
先上代码:
bool is_prime(int x) {
if (x < 2) return false;
for (int i = 2; i < x; ++i)
if (x % i == 0)
return false;
return true;
}
很显然,
0
0
0和
1
1
1都不是质数,这就是第一个判断所做的。
在判断一个数是否是质数时,我们只需要判断它是否有除平凡约数(就是
1
1
1和它本身)外的其他约数即可,这就是循环所做的。
这样的时间复杂度是
O
(
n
)
O(n)
O(n)。
优化
显然对于一个合数
x
x
x,一定存在一个质数
p
≤
x
p \leq \sqrt{x}
p≤x且
p
∣
x
p|x
p∣x。
那么我可以将枚举上限设为
x
\sqrt{x}
x,那样一样可以找到
x
x
x的一个因数。
代码如下:
bool is_prime(int x) {
if (x < 2) return false;
for (int i = 2; i * i <= x; ++i) // 等价于 i <= sqrt(x)
if (x % i == 0)
return false;
return true;
}
这样的时间复杂度是 O ( n ) O(\sqrt{n}) O(n)。
埃氏筛
用于确定 [ 1 , n ] [1, n] [1,n]内的质数。
原理很简单,就是质数的倍数即为合数。
那如何实现呢?或者更具体说,如何“先”找到质数?
- 先给每个数【都】打上质数标记 i s P r i m e [ i ] isPrime[i] isPrime[i],表示数 i i i是质数。
- 外层循环遇到一个质数。
- 进入内层循环把【这个质数的倍数】(就是含有该质因数的合数)的“质数标记”删去。
- 当外层循环完毕时,剩下【有“质数标记”的】就是质数。
先上代码:
int n;
int prime[maxn], cnt; // 用来存质数
bitset<maxn> isPrime; // 质数标记:1 表是质数,0 表不是质数
// 用 bitset 更省空间
// 相当于:bool isPrime[maxn]
void Eratosthenes() {
isPrime.set();
// 相当于:memset(isPrime, true, sizeof(isPrime))
isPrime[0] = isPrime[1] = false; // 0 和 1 均不为质数
// 实现 1
for (int i = 2; i <= n; ++i) {
if (isPrime[i]) {
prime[++cnt] = i;
for (int j = i * 2; j <= n; j += i)
isPrime[j] = false;
}
}
// 实现 2
// for (int i = 2; i <= n; ++i) {
// if (isPrime[i]) prime[++cnt] = i;
// for (int j = 1; j <= cnt && i * prime[j] <= n; ++j)
// isPrime[i * prime[j]] = false;
// }
// 实现 2 主要是为了方便下来讲解在此基础上实现的线性筛
}
正确性
此做法的正确性来源于每个合数都至少有一个质因数(已经证明过了),而每个质数的因子都只有 1 1 1和它自己。
想一想当前外层循环运行到
i
=
x
i=x
i=x时:
如果它被筛去了,由于循环从
2
2
2开始,所以它不可能被
1
1
1筛去,那么说明筛去它的是一个大于
1
1
1且小于它自己的数,换而言之它存在除平凡因数外的因数,它是个合数;
如果它没被筛去,那么说明它不存在除平凡因数外的因数,它是个质数。
这就证明了埃氏筛的正确性:
- 留下的都是质数
- 筛去的都是合数和 01 01 01
根据证明我们可以明白一件事: x x x是质数还是合数在外层循环运行到 i = x i=x i=x前就可以得到判断;当外层循环运行到 i = x i=x i=x时, [ 2 , x ] [2,x] [2,x]间所有数是质是合都可得到判断。
优化
注:以下优化均以实现1为基础
优化1
先证明一个东西:
[ 1 , i ] 以内的质数可以把 [ 1 , i 2 ] 内的合数全部筛掉。 [1, i]以内的质数可以把[1,i^2] 内的合数全部筛掉。 [1,i]以内的质数可以把[1,i2]内的合数全部筛掉。
很显然当外层循环到
i
i
i时,
[
1
,
i
]
[1,i]
[1,i]内的所有质数和合数均已确定。
现在考虑在
(
i
,
i
2
]
(i, i^2]
(i,i2]的一个合数
x
x
x,
根据每个合数至少存在一个质因子,且小于等于
这个合数
\sqrt{这个合数}
这个合数,
我们可知
x
x
x有个质因子小于等于
x
\sqrt{x}
x,即在
[
1
,
i
]
[1,i]
[1,i]区间内,
由于外层循环已经确定了
[
1
,
i
]
[1,i]
[1,i]的所有质数,
因此我们可以再通过内层循环将
(
i
,
i
∗
i
]
(i, i * i]
(i,i∗i]内的合数筛去。
所以外层循环上限可以改为:
for(int i = 2; i * i <= n; ++i)
延续思路,当 i i i为质数进入内层循环时,由于 [ 1 , i − 1 ] [1,i-1] [1,i−1]的质数已经把 [ 1 , ( i − 1 ) 2 ] [1,(i-1)^2] [1,(i−1)2]的合数筛去了,所以我们 j j j 要找到一个【大于且离 ( i − 1 ) 2 (i-1)^2 (i−1)2最近的】 i i i的倍数作为循环起点,显然起点为 i ( i − 1 ) i(i-1) i(i−1),但它又很明显是 i − 1 i-1 i−1的倍数,因此当 i − 1 ≠ 1 i-1\neq1 i−1=1即 i ≠ 2 i\neq2 i=2时它一定也被筛去了,所以循环起点定为 i 2 i^2 i2。
for (int j = i * i; j <= n; j += i)
注:当 i = 2 i=2 i=2时, i ( i − 1 ) = 2 i(i-1)=2 i(i−1)=2,此时若再将其作为内层循环起点就会把 i s P r i m e [ 2 ] isPrime[2] isPrime[2]标记为 f a l s e false false,显然错误。
优化2
显然所有偶数均为合数,所以只需判断一个奇数是否为质数。
结合优化1给出如下代码:
void Eratosthenes() {
isPrime.set();
isPrime[0] = isPrime[1] = false;
for (int i = 4; i <= n; i += 2)
isPrime[i] = false; // 严谨一点把的非 2 偶数 isPrime 也删除一下
for (int i = 3; i * i <= n; i += 2) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i)
isPrime[j] = false;
}
}
prime[++cnt] = 2;
for (int i = 3; i <= n; i += 2)
if (isPrime[i])
prime[++cnt] = i;
}
时间复杂度为
O
(
n
l
n
(
l
n
(
n
)
)
)
O(nln(ln(n)))
O(nln(ln(n))),具体证明。
注:优化只能省去一些不必要的操作,并不能改变复杂度。
线性筛(欧拉筛)
同样用于确定 [ 1 , n ] [1, n] [1,n]内的质数。
是在埃氏筛实现2的基础上实现的。
先给代码:
void Euler() {
isPrime.set();
isPrime[0] = isPrime[1] = false;
for (int i = 2; i <= n; ++i) {
if (isPrime[i]) prime[++cnt] = i;
for (int j = 1; j <= cnt && i * prime[j] <= n; ++j) {
isPrime[i * prime[j]] = false;
if (i % prime[j] == 0) break; // 关键
}
}
}
看来线性复杂度的秘诀就在这个关键判断语句上。
下来给出证明:
显然埃氏筛会重复的筛去相同的质数,
如: 考虑
2
2
2时,它会筛去
6
,
12
,
18...
6,12,18...
6,12,18...
考虑
3
3
3时,它还会筛去
6
,
12
,
18...
6,12,18...
6,12,18...
这就导致了时间复杂度会乘以
l
n
(
l
n
(
n
)
)
ln(ln(n))
ln(ln(n)).
那为什么这行判断会省去多余的筛次,
如果
p
r
i
m
e
[
j
]
∣
i
prime[j]|i
prime[j]∣i,
设
m
=
i
/
p
r
i
m
e
[
j
]
m=i/prime[j]
m=i/prime[j],即
i
=
x
∗
p
r
i
m
e
[
j
]
i=x*prime[j]
i=x∗prime[j],
那么
i
∗
p
r
i
m
e
[
j
+
k
]
=
x
∗
p
r
i
m
e
[
j
]
∗
p
r
i
m
e
[
j
+
k
]
i*prime[j+k]=x*prime[j]*prime[j+k]
i∗prime[j+k]=x∗prime[j]∗prime[j+k],
(
1
≤
k
≤
n
−
j
1 \leq k \leq n-j
1≤k≤n−j,
p
r
i
m
e
[
j
+
k
]
prime[j+k]
prime[j+k]其实指的就是
p
r
i
m
e
prime
prime数组中剩下的质数),
也就是说
i
∗
p
r
i
m
e
[
j
+
k
]
i*prime[j+k]
i∗prime[j+k]会被
p
r
i
m
e
[
j
]
prime[j]
prime[j]筛去,还会被
p
r
i
m
e
[
j
+
k
]
prime[j +k]
prime[j+k]筛去,既然它已经被筛了,那就不必麻烦
p
r
i
m
e
[
j
+
k
]
prime[j+k]
prime[j+k]再把它筛一遍,所以直接
b
r
e
a
k
break
break。
时间复杂度正如它的名字: O ( n ) O(n) O(n)。