好的,咱们来聊聊埃氏筛法为啥效率这么高,能达到 $O(n log log n)$ 这个让人觉得有点炫酷的复杂度。这事儿得从头说起,一点一点拆解。
什么是埃氏筛法?
首先,你得明白埃氏筛法是干嘛的。它的主要目标是从 1 到 n 这样一个区间里,把所有的质数都找出来。它不像咱们一个个数去试能不能被整除那么费劲,而是采取一种“筛”的方式,一遍遍地划掉合数。
基本思路
1. 一个数组,从 2 开始,把所有数都当成可能是质数。
2. 找到第一个未被划掉的数,比如 2。这个数就是质数。
3. 然后,把这个质数的所有倍数(4, 6, 8, 10...)都划掉,因为它们肯定是合数。
4. 继续往下找,找到下一个未被划掉的数,比如 3。这个数也是质数。
5. 再把 3 的所有倍数(6, 9, 12, 15...)划掉。注意,有些已经被划掉了(比如 6),没关系,重复划也无妨。
6. 重复这个过程,直到你处理到根号 n 就可以了。为啥是根号 n 呢?因为如果一个数 N 是合数,它肯定有一个小于等于根号 N 的因子。如果它比根号 N 都要大,那它另一个因子就必然小于根号 N 了,那个小因子早就已经把 N 划掉了。
为什么不是 $O(n^2)$ 或者 $O(n log n)$?
你想想,如果咱们真的去对每一个数进行试除,比如检查 1 到 n 的每一个数是否能被比它小的数整除,那复杂度会很高。
一个朴素的方法是,对于每个数 i 从 2 到 n,再去检查从 2 到 i1 的每个数 j,看看 i 是否能被 j 整除。这样的话,每个数都可能需要做 O(i) 次判断,总共就是 O(n^2) 的复杂度了,效率很低。
稍微好一点,我们知道只需要检查到 $sqrt{i}$ 就可以了,这样对于每个数 i,需要 O($sqrt{i}$) 的操作。对所有数求和下来,复杂度大约是 $O(n sqrt{n})$。
更进一步,如果咱们只用质数去除,也就是我们说的埃氏筛法的“筛”的逻辑,那复杂度会进一步降低。
揭秘 $O(n log log n)$
现在咱们重点来看看埃氏筛法是如何做到 $O(n log log n)$ 的。这涉及到两个关键的部分:
1. 筛掉倍数的操作总次数。
2. 质数分布的规律。
咱们一步步来分析:
第一步:计算筛掉倍数的操作总次数
想象一下,我们从 2 开始,把 2 的倍数都划掉:$2 imes 2, 2 imes 3, 2 imes 4, dots, 2 imes (n/2)$。 大概是 $n/2$ 个数。
然后是 3,把 3 的倍数划掉:$3 imes 2$ (已经划了), $3 imes 3, 3 imes 4, dots, 3 imes (n/3)$。 大概是 $n/3$ 个数。
接着是 5,把 5 的倍数划掉:$5 imes 2$ (已经划了), $5 imes 3$ (已经划了), $dots, 5 imes k$, 直到 $5k le n$。 大概是 $n/5$ 个数。
依此类推,对于每一个质数 $p le n$,我们都会去划掉它的倍数,也就是 $p imes 2, p imes 3, p imes 4, dots$ 直到 $p imes k le n$。 这意味着我们对 $p$ 的倍数进行了 $n/p 1$ 次操作(因为 $p imes 1 = p$ 本身是质数,不用划)。
所以,我们总共要做的“划掉倍数”的操作次数,大致可以写成这样一个求和:
$S = frac{n}{2} + frac{n}{3} + frac{n}{5} + frac{n}{7} + dots$ (这里只列出了质数作为筛子的基数)
我们把这个式子写成更数学一点的形式:
$S approx n left( frac{1}{2} + frac{1}{3} + frac{1}{5} + frac{1}{7} + dots + frac{1}{p}
ight)$,其中 $p le n$ 且 $p$ 是质数。
第二步:质数分布的规律和调和级数
现在关键来了:那个 $left( frac{1}{2} + frac{1}{3} + frac{1}{5} + frac{1}{7} + dots + frac{1}{p}
ight)$ 这部分是怎么算的?
这里涉及到 素数定理 的一个推论,以及 调和级数 的概念。
调和级数:咱们都知道,级数 $1 + frac{1}{2} + frac{1}{3} + frac{1}{4} + dots$ 是发散的,它的部分和大约是 $ln n$。 具体来说,$sum_{i=1}^n frac{1}{i} approx ln n$。
质数的倒数和:欧拉证明过,所有质数的倒数和也是发散的,而且它的增长速度和调和级数非常相似。 具体来说,所有小于等于 n 的质数的倒数和:
$sum_{p le n, p ext{ is prime}} frac{1}{p} approx ln(ln n)$
这个 $ln(ln n)$ 是一个增长非常缓慢的函数。 你可以理解为:
n 增大 10 倍, $ln n$ 增长的量是 $ln 10$。
n 增大 10 倍, $ln(ln n)$ 增长的量是 $ln(ln(10n)) ln(ln n) = ln(frac{ln(10n)}{ln n}) = ln(1 + frac{ln 10}{ln n})$,这个量非常小。
把它们结合起来
回到我们之前计算的操作总次数:
$S approx n left( frac{1}{2} + frac{1}{3} + frac{1}{5} + frac{1}{7} + dots + frac{1}{p}
ight)$
我们知道前面那个括号里的和是 $ln(ln n)$ 的量级。 所以,总的操作次数就是 $n imes ln(ln n)$ 的量级。
为什么是 $O(n log log n)$ 而不是精确的 $n ln ln n$?
在算法分析里,我们通常用大 O 记号来表示一个算法渐进的增长趋势,忽略常数因子和低阶项。 $O(n log log n)$ 就抓住了这个核心的增长速度。
另外,还有一个小细节:埃氏筛法会重复筛。比如 6 被 2 筛了,也被 3 筛了。 但是,这并不影响最终的复杂度分析,因为即使是重复筛,我们本质上还是在遍历那些合数。关键在于,我们是遍历 质数 的倍数来做筛除,而不是遍历 所有数 去判断是否是质数。 遍历质数这个过程本身,质数在数轴上的分布就决定了后面那一项 $log log n$ 的出现。
总结一下
埃氏筛法的 $O(n log log n)$ 复杂度,可以理解为:
1. 我们对每一个小于等于 $n$ 的质数 $p$,执行一次筛除操作。这个操作涉及划掉 $p$ 的倍数,大概耗费 $n/p$ 的工作量。
2. 所有质数倒数和 $sum_{p le n} frac{1}{p}$ 的增长趋势是 $log log n$。
3. 所以,总的操作次数就是 $n imes (sum frac{1}{p}) approx n log log n$。
这个复杂度比我们之前提到的任何方法都要好,因为它有效地利用了质数的分布规律,并且避免了不必要的重复判断。 整个过程就像是在对一个大池塘里的鱼进行筛选,你不是一条条去抓,而是找出那些会产卵的母鱼,然后把它们的水域标记出来,间接地就把很多小鱼(合数)给解决了。 效率自然就高了。