Sieve

质数筛法详解:从埃氏筛到欧拉筛的优化之路

在信息学竞赛中,高效获取一定范围内的质数是解决数论问题的基础。筛法(Sieve)是实现这一目标的核心技术,本文将详细讲解两种经典筛法 —— 埃拉托斯特尼筛法(Eratosthenes Sieve)和欧拉筛(Euler Sieve,又称线性筛)的原理、实现及优化思路。

一、埃拉托斯特尼筛法(Eratosthenes Sieve)

1.1 基本原理

埃氏筛的核心思想是 「标记合数」:从最小的质数 2 开始,将每个质数的所有倍数标记为合数,未被标记的数即为质数。

具体步骤

  1. 初始化一个布尔数组 \(vis\)\(vis[i]\) 表示 \(i\) 是否为合数(初始全为 \(false\),即默认都是质数);
  2. 从 2 开始遍历到 \(n\)
    • \(i\) 未被标记(即 \(vis[i] = false\)),则 \(i\) 是质数;
    • \(i\) 的所有倍数(从 \(2i\) 开始,步长为 \(i\))标记为合数;
  3. 遍历结束后,所有未被标记的数即为 \([2, n]\) 范围内的质数。

1.2 代码实现

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;
int n;
bool vis[N];  // vis[i] = true 表示i是合数

void Eratosthenes_Sieve(int n) {
    vis[1] = true;  // 1不是质数
    for (int i = 2; i <= n / i; i ++ )  // 外层循环到√n即可
        if (!vis[i])  // i是质数
            // 标记i的所有倍数为合数
            for (int j = i + i; j <= n; j += i)
                vis[j] = true;
}

int main() {
    cin >> n;
    Eratosthenes_Sieve(n);
    // 输出所有质数(未被标记的数)
    for (int i = 2; i <= n; i ++ )
        if (!vis[i])
            cout << i << " ";

    return 0;
}

1.3 关键优化解析

  • 外层循环到 \(\sqrt{n}\):对于大于 \(\sqrt{n}\) 的合数 \(x\),其最小质因子一定小于 \(\sqrt{n}\),因此在处理小于 \(\sqrt{n}\) 的质数时,\(x\) 已被标记。例如 \(x=25\),其最小质因子是 5(\(<\sqrt{25}=5\)),在 \(i=5\) 时会被标记。
  • \(i \times i\) 开始标记:进一步优化可将内层循环起点从 \(i+i\) 改为 \(i \times i\)(因为 \(2i, 3i, \dots, (i-1)i\) 已被更小的质数标记过)。例如 \(i=5\) 时,\(5 \times 2=10\)(被 2 标记)、\(5 \times 3=15\)(被 3 标记),从 \(5 \times 5=25\) 开始标记即可。

1.4 时间复杂度

埃氏筛的时间复杂度为 \(O(n \log \log n)\),接近线性。证明涉及数论中的调和级数,直观理解:每个质数 \(p\) 的标记次数为 \(n/p\),总操作数约为 \(n(1/2 + 1/3 + 1/5 + \dots + 1/p)\)(质数倒数和),而该和的极限为 \(\log \log n\)

二、欧拉筛(Euler Sieve,线性筛)

埃氏筛虽高效,但存在重复标记问题(例如 12 会被 2 和 3 分别标记)。欧拉筛通过保证每个合数只被其最小质因子标记一次,将时间复杂度优化至严格线性 \(O(n)\)

2.1 核心改进:最小质因子标记

欧拉筛引入一个数组 \(primes\) 存储已找到的质数,通过以下规则避免重复标记:

对于每个数 \(i\)(从 2 到 \(n\)):

  1. \(i\) 是质数(未被标记),加入 \(primes\) 数组;
  2. 遍历 \(primes\) 中已有的质数 \(p\),标记 \(i \times p\) 为合数;
  3. \(i \% p == 0\) 时,\(p\)\(i\) 的最小质因子,因此也是 \(i \times p\) 的最小质因子,此时停止遍历(避免用更大的质数标记 \(i \times p\))。

2.2 代码实现

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;
int n, cnt;  // cnt为质数个数
int primes[N];  // 存储质数
bool vis[N];    // 标记合数

void Euler_Sieve(int n) {
    vis[1] = true;  // 1不是质数
    for (int i = 2; i <= n; i ++ ) {
        // i是质数,加入primes
        if (!vis[i]) primes[ ++ cnt] = i;

        // 用已找到的质数标记i的倍数
        for (int j = 1; primes[j] <= n / i; j ++ ) {
            vis[i * primes[j]] = true;  // 标记i*primes[j]为合数
            // 关键:primes[j]是i的最小质因子
            if (i % primes[j] == 0) break;
        }
    }
}

int main() {
    cin >> n;
    Euler_Sieve(n);
    // 输出所有质数
    for (int i = 1; i <= cnt; i ++ )
        cout << primes[i] << " ";
        
    return 0;
}

2.3 去重原理详解

为什么 \(i \% primes[j] == 0\) 时要 break?

  • \(i \% primes[j] == 0\),说明 \(primes[j]\)\(i\) 的因子,且是最小质因子(因为 \(primes\) 中的质数递增);
  • 此时 \(i \times primes[j+1]\) 的最小质因子仍是 \(primes[j]\)(而非 \(primes[j+1]\)),若继续循环,会用更大的质数标记 \(i \times primes[j+1]\),导致重复标记;
  • 例如 \(i=6\)(质因子 2、3),\(primes[j]=2\)\(6\%2=0\),此时 \(6 \times 3=18\) 的最小质因子是 2(而非 3),应在 \(j=1\) 时 break,避免 \(j=2\) 时用 3 标记 18(18 会在 \(i=9\)\(j=1\) 时被 2 标记)。

2.4 时间复杂度

由于每个合数恰好被标记一次(被其最小质因子),总操作数为 \(O(n)\),因此欧拉筛是严格线性的筛法。

三、两种筛法的对比与应用场景

特性 埃氏筛(Eratosthenes) 欧拉筛(Euler)
时间复杂度 \(O(n \log \log n)\) \(O(n)\)
空间复杂度 \(O(n)\) \(O(n)\)(需额外存储质数)
重复标记 存在
适用场景 中小范围(\(n \leq 10^7\) 大范围(\(n > 10^7\)

竞赛中的选择建议

  • \(n > 2 \times 10^6\)时,欧拉筛的线性复杂度优势明显。
  • 竞赛中几乎不可能只让你筛出 \(1\sim n\) 的质数,但是埃氏筛不能额外储存质因子,而且遍历还需要 \(O(n)\) 的额外时间,在竞赛中极少使用。
  • 强烈推荐欧拉筛。

四、扩展:筛法的实际应用

  • 质因数分解加速:结合欧拉筛记录的最小质因子,可在 \(O(\log n)\) 时间内分解任意数;
  • 数论函数预处理:如欧拉函数 \(\varphi(n)\)、莫比乌斯函数 \(\mu(n)\) 等,可通过筛法线性预处理;
  • 区间质数筛选:对于 \([L, R]\)\(L\) 较大),可先用筛法处理 \([2, \sqrt{R}]\),再标记区间内的合数。
posted @ 2025-08-30 19:29  wz150432  阅读(0)  评论(0)    收藏  举报