欧拉筛
核心思想
埃氏筛(Eratosthenes Sieve)的效率瓶颈在于:一个合数可能会被其不同的质因子多次重复筛除。
例如:
欧拉筛(Linear Sieve)的核心思想非常简单且强力:
确保每个合数只被它的“最小质因子”筛除一次。
为了实现这一点,我们需要在筛的过程中维护两个关键操作:
- 外层枚举:从小到大枚举数值
。 - 内层标记:枚举已有的素数
,标记 为合数。 - 关键中断:当发现
已经被某个素数 整除时,立即停止后续标记(Break)。
数学解析:唯一性分解的证明
为了证明算法的线性时间复杂度(不重复筛选),我们需要建立合数与筛选操作之间的一一映射关系。
定义
设
其中:
是 的最小质因子。 是除以最小质因子后的商。
我们的目标是:对于任意合数
证明:一一映射的存在性与唯一性
为了证明算法既不漏(覆盖所有合数)又不重(线性复杂度),我们需要证明“筛选操作”与“合数”之间存在双向的一一对应关系。
定义映射
1. 存在性证明(对应“不漏”,满射)
- 命题:对于任意合数
,存在一对符合欧拉筛规则的 生成它。 - 证明:由算术基本定理,任意合数
必有一个最小质因子 。令 。显然 成立(否则 不是最小质因子)。因此,每一个合数 都能被算法生成的某对 筛掉。
2. 唯一性证明(对应“不重”,单射)
- 命题:对于任意合数
,生成它的 是唯一的。 - 证明:假设
能被 和 两次筛掉,且都满足欧拉筛规则(即 是 的最小质因子)。 - 这意味着
是 的最小质因子, 也是 的最小质因子。 - 由最小质因子的唯一性,得
。 - 进而
。 - 故
与 是同一对。
- 这意味着
- 结论:每个合数只会被其“最小质因子”筛去一次,绝无重复。
这篇文章在上一版的基础上,补充了你强调的**“性质推导”和“集合选择”**的逻辑,着重解决了从“数学定义”到“算法实现”的过渡,让“为什么要在 i % p == 0 时 break”这一行为有了严密的数学依据。
以下是完善后的文章:
2. 核心原理与证明
核心思想:唯一性筛选
埃氏筛(Eratosthenes Sieve)的效率瓶颈在于:一个合数可能会被其不同的质因子多次重复筛除。
例如:
欧拉筛(Linear Sieve)的核心思想非常简单且强力:
确保每个合数只被它的“最小质因子”筛除一次。
为了实现这一点,我们需要在筛的过程中维护两个关键操作:
- 外层枚举:从小到大枚举数值
(作为最大因子的候选者)。 - 内层标记:枚举已有的素数
,将 标记为合数。 - 关键中断:当发现
已经被某个素数 整除时,立即停止后续标记(Break)。
数学解析:建立一一映射
为了证明算法的线性时间复杂度(即每个合数只被筛一次),我们需要建立合数与“筛选操作”之间的一一映射关系。
1. 定义与分解
设
对于任意合数
其中:
是 的最小质因子(筛选时的素数 )。 是除以最小质因子后的商(筛选时的倍数 )。
我们的目标是:对于任意合数
2. 证明:一一映射的存在性与唯一性
命题:每个合数
证明:
-
存在性: 由算术基本定理可知,任何合数
都有唯一的质因数分解,必然存在一个最小的质因子。 令 ,令 。显然 且 是 的最小质因子。因此,该组合 存在。 -
唯一性: 假设存在两对不同的组合
和 都能筛选出 ,且都遵循欧拉筛规则(即 必须是 的最小质因子)。 根据定义, 且 。 因为一个数的最小质因子是唯一的,所以 。 又因为 且 ,推导出 。 故分解方式是唯一的。
算法推导:如何确定 的集合?
有了上述映射关系,接下来的核心问题是:
当我们枚举到一个具体的数
我们需要找到满足条件的素数集合
性质推导
为了保证
限制条件:
证明:
假设我们选用的素数
因此,对于固定的
实例演示 ( )
假设当前枚举到
已有的素数表为 primes = [2, 3, 5, 7, ...] 。
-
枚举
: ,满足条件。 - 筛掉
。 - 验证:
的最小质因子确实是 。✅
-
枚举
: ,满足条件。 - 筛掉
。 - 验证:
的最小质因子确实是 。✅ - 触发边界:发现
( ),说明 已经达到了 的上限。
-
若强行枚举
(反例): ,违反条件。 - 若筛掉
。 的最小质因子是 (来自 ),而不是我们当前使用的 。 - 这意味着
应该在未来当 ( ) 时被筛除,而不是现在。❌
补充性质:
在欧拉筛的运行过程中,生成的合数分解
证明:
因为
算法演进:从重复到唯一
1 朴素的“倍数法”(存在重复)
我们先看一段试图用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
std::vector<int> primes;
bool st[MAX_N];
void get_primes_naive(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes.push_back(i); // i 是素数
// 枚举已有的素数 p,构造合数 i * p
for (auto p : primes) {
if (p * i > n) break; // 越界停止
st[p * i] = true; // 标记合数
// 调试输出:观察谁筛掉了谁
// printf("[%d] = %d x %d\n", p * i, p, i);
}
}
}
问题分析:
以
- 当
时,枚举到 (此时 primes中有 2, 3)。计算。虽然标记了 12,但此时使用的素数 不是 的最小质因子( 才是)。 - 当
时,枚举到 。计算 。此时 是 的最小质因子。
这就违反了“一一映射”原则,导致了重复计算。
2 欧拉筛的修正(引入 Break)
为了修正上述问题,我们必须保证构造
核心修正逻辑:
在内层循环枚举素数 i % p == 0,说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void get_primes_euler(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes.push_back(i);
for (int j = 0; j < primes.size(); j++) {
int p = primes[j];
if (i * p > n) break;
st[i * p] = true;
// 【核心关键】
// 如果 i 被 p 整除,说明 i 的最小质因子就是 p。
// 如果继续枚举下一个素数 p_next (p_next > p),
// 构造出的合数 X = i * p_next。
// X 的最小质因子将会是 p (因为 i 包含因子 p),而不是 p_next。
// 这违背了“用最小质因子筛除”的原则,因此必须 break。
if (i % p == 0) break;
}
}
}
为什么 i % p == 0 时必须 Break?
我们可以通过反证法来理解这个 Break 的必要性。
假设当 i % p == 0 时,我们不停止,继续枚举下一个更大的素数
因为
请注意
但在当前的循环中,我们是试图用
例子:
当 primes 为
: 。 的最小质因子是 ,合法。标记 。 检测 4 % 2 == 0,触发 Break。- 假如不 Break,继续
: 计算 。 对于 来说,最小质因子是 。但这里我们试图用 去筛它。 这就是多余的操作。 应该在 时,由 筛除。
总结
欧拉筛通过简单的 if (i % p == 0) break; 实现了对合数分解唯一性的严格控制:
- 当
i % p != 0:说明小于 的所有质因子。此时 必定是 的最小质因子。继续筛选。 - 当
i % p == 0:说明是 的最小质因子。此时 的最小质因子是 。但如果换用更大的素数 ,合数 的最小质因子依然是 (来自 ),而不是 。为了避免重复,必须停止。