筛法
一个证明
在讲筛法前,先证明一个东西:
-
先把结论考虑简单一点:合数\(x\)一定存在一个小于等于\(\sqrt{x}\)的因数。
显然一个合数\(x\)可以被一对非\(1\)非\(x\)的整数\(p,q\)表示为 \(x=pq\),
则其中至少有一个整数小于\(x\),
因为若\(p,q > \sqrt{x}\),那么\(pq > x\)。
-
再进一步考虑这个结论:合数\(x\)一定存在一个小于等于\(\sqrt{x}\)的质因数。
我们先假设每个合数不存在非\(1\)质因子,
对于合数\(x\),它一定有一个的小于等于\(\sqrt{x}\)的非\(1\)因数\(p\),
而已经假设了每个合数不存在质因子,所以\(p\)为合数,
那么对于合数\(p\), 它一定又有一个小于等于\(\sqrt{p}\)的非\(1\)合数因子\(p_1\),
则\(p_1\)也为\(a\)的因子,
同上可得:\(p_1\)也一定有一个小于等于\(\sqrt{p_1}\)的非\(1\)合数因子\(p_2\),
\(p_2\)也为\(a\)的因子,
......
如此循环往复可以做到\(a\)的一个因子\(p_n\leq\sqrt{\sqrt{\sqrt{\sqrt{...}}}}\),
显然不等号右边的式子可以轻而易举地做到【大于 1 且 小于 2】,由于\((1, 2)\)区间内不存在合数,所以假设不成立。
筛法
筛法是对质数和合数进行分类的工具。
假如将所有整数放进一个筛子,那么这个筛子会筛掉合数,留下质数。
一个正确的筛法要同时满足两点:
- 留下的都是质数
- 筛去的都是合数和\(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\)和\(1\)都不是质数,这就是第一个判断所做的。
在判断一个数是否是质数时,我们只需要判断它是否有除平凡约数(就是\(1\)和它本身)外的其他约数即可,这就是循环所做的。
这样的时间复杂度是\(O(n)\)。
优化
显然对于一个合数\(x\),一定存在一个质数\(p \leq \sqrt{x}\)且 \(p|x\)。
那么我可以将枚举上限设为\(\sqrt{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(\sqrt{n})\)。
埃氏筛
用于确定\([1, n]\)内的质数。
原理很简单,就是质数的倍数即为合数。
那如何实现呢?或者更具体说,如何“先”找到质数?
- 先给每个数【都】打上质数标记\(isPrime[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\)和它自己。
想一想当前外层循环运行到\(i=x\)时:
如果它被筛去了,由于循环从\(2\)开始,所以它不可能被\(1\)筛去,那么说明筛去它的是一个大于\(1\)且小于它自己的数,换而言之它存在除平凡因数外的因数,它是个合数;
如果它没被筛去,那么说明它不存在除平凡因数外的因数,它是个质数。
这就证明了埃氏筛的正确性:
- 留下的都是质数
- 筛去的都是合数和\(01\)
根据证明我们可以明白一件事:\(x\)是质数还是合数在外层循环运行到\(i=x\)前就可以得到判断;当外层循环运行到\(i=x\)时,\([2,x]\)间所有数是质是合都可得到判断。
优化
注:以下优化均以实现1为基础
优化1
先证明一个东西:
很显然当外层循环到\(i\)时,\([1,i]\)内的所有质数和合数均已确定。
现在考虑在\((i, i^2]\)的一个合数\(x\),
根据每个合数至少存在一个质因子,且小于等于\(\sqrt{这个合数}\),
我们可知\(x\)有个质因子小于等于 \(\sqrt{x}\),即在 \([1,i]\)区间内,
由于外层循环已经确定了\([1,i]\)的所有质数,
因此我们可以再通过内层循环将\((i, i * i]\)内的合数筛去。
所以外层循环上限可以改为:
for(int i = 2; i * i <= n; ++i)
延续思路,当\(i\)为质数进入内层循环时,由于\([1,i-1]\)的质数已经把\([1,(i-1)^2]\)的合数筛去了,所以我们\(j\) 要找到一个【大于且离\((i-1)^2\)最近的】\(i\)的倍数作为循环起点,显然起点为\(i(i-1)\),但它又很明显是\(i-1\)的倍数,因此当\(i-1\neq1\)即\(i\neq2\)时它一定也被筛去了,所以循环起点定为\(i^2\)。
for (int j = i * i; j <= n; j += i)
注:当\(i=2\)时,\(i(i-1)=2\),此时若再将其作为内层循环起点就会把\(isPrime[2]\)标记为\(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(nln(ln(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\)时,它会筛去\(6,12,18...\)
考虑\(3\)时,它还会筛去\(6,12,18...\)
这就导致了时间复杂度会乘以\(ln(ln(n))\).
那为什么这行判断会省去多余的筛次,
如果\(prime[j]|i\),
设 \(m=i/prime[j]\),即\(i=x*prime[j]\),
那么\(i*prime[j+k]=x*prime[j]*prime[j+k]\),
(\(1 \leq k \leq n-j\),\(prime[j+k]\)其实指的就是\(prime\)数组中剩下的质数),
也就是说\(i*prime[j+k]\)会被\(prime[j]\)筛去,还会被\(prime[j +k]\)筛去,既然它已经被筛了,那就不必麻烦\(prime[j+k]\)再把它筛一遍,所以直接\(break\)。
时间复杂度正如它的名字:\(O(n)\)。

浙公网安备 33010602011771号