← 返回文章
埃拉托斯特尼篩法

埃拉托斯特尼篩法

Sieve of Eratosthenes & Linear Sieve

埃篩 O(n log log n)
線性篩 O(n)

什麼是埃拉托斯特尼篩法?

埃拉托斯特尼篩法是一種用來生成質數篩法,得名於古希臘數學家 埃拉托斯特尼
— 維基百科

簡單說:一個古老的 質數篩選演算法,用來找出某個範圍內所有的質數。

「埃拉托斯特尼篩法」念起來和輕小說標題一樣長,通常簡稱為 埃篩

質數的定義

質數是指在大於 1 的自然數中,除了 1 和該數自身外,無法被其他自然數整除的數。
— 維基百科
數字 因數 是否為質數
7 1, 7 ✓ 是
12 1, 2, 3, 4, 6, 12 ✗ 否(合數)
1 1 既非質數也非合數

小練習

題目: 使用任何程式語言,不限方法,列印出 1 ~ 114514 之間的所有質數。

為什麼需要埃篩?

直覺上,判斷 n 是否為質數可以這樣寫:

bool is_prime(int n){
    if(n < 2) return false;
    for(int i = 2 ; i * i <= n ; ++i){
        if(n % i == 0)
            return false;
    }
    return true;
}
💡 小技巧
i * i <= n 而非 i <= sqrt(n)
→ 避免浮點數精度問題
問題
單次查詢:\(O(\sqrt{n})\)
查詢 n 個數:\(O(n\sqrt{n})\)
→ 大量查詢時太慢!
Part 01

埃拉托斯特尼篩法

時間複雜度 \(O(n \log \log n)\),透過逐步篩除倍數找出所有質數

埃拉托斯特尼篩法動畫
找出 2 ~ 120 內所有質數的過程

以 1 ~ 30 為例

1
2 是質數,標記 4, 6, 8 ... 為合數
2
3 未被標記 → 是質數,標記 9, 15, 21, 27
3
4 已被標記 → 跳過
4
5 未被標記 → 是質數,標記 25
5
篩到 \(\sqrt{30}\approx5\) 停止 ←為什麼?
6
剩下未標記的就是質數 🎉
質數 正在處理 合數

小練習:標記 1 ~ 60 的質數

點擊「下一步」逐步查看篩法過程
質數 正在處理 合數

為什麼只篩到 \(\sqrt{n}\)?

目標

對於範圍 \([2, n]\) 內的每一個合數 \(c\),它必定會被某個 \(\leq \sqrt{n}\) 的質數篩掉。

前置知識

  • 質數:只能被 1 和自身整除的整數(\(\geq 2\))
  • 合數:除了 1 和自身之外,還有其他因數(\(\geq 2\))
  • 算術基本定理:任何大於 1 的整數都可以唯一分解成質因數乘積
    • 例:\(12 = 2^2 \times 3\)

核心引理

若 \(c\) 是合數,則 \(c\) 的最小質因數 \(p\) 滿足 \(p \leq \sqrt{c}\)

證明(反證法):假設合數 \(c\) 的所有質因數都 \(> \sqrt{c}\),那麼至少有兩個質因數 \(r, k\):

\[ c = r \times k \times \cdots \]
\[ r > \sqrt{c}, \quad k > \sqrt{c} \] \[ \Rightarrow c > r \times k > \sqrt{c} \times \sqrt{c} = c \]
得到 \(c > c\),矛盾!
所以至少有一個質因數 \(\leq \sqrt{c}\), 因此最小質因數 \(p\) 必定 \(\leq \sqrt{c}\)

主定理

篩法只需對 \([2, \sqrt{n}]\) 內的質數執行篩除,即可找出 \([2, n]\) 內所有質數。

證明:取 \([2, n]\) 內任意合數 \(c\)

Step 1 \(c\) 有最小質因數 \(p\)
Step 2 由引理:\(p \leq \sqrt{c}\)
Step 3 因為 \(c \leq n\),所以 \(p \leq \sqrt{c} \leq \sqrt{n}\)
Step 4 篩法會處理所有 \(\leq \sqrt{n}\) 的質數,\(p\) 會被處理到
Step 5 \(c\) 是 \(p\) 的倍數 → 篩法處理 \(p\) 時,\(c\) 必定被標記 ✓
所有合數都被篩掉 → 剩下未標記的全是質數! 🎉

埃篩程式模板

#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 1e7 + 5;
bool is_composite[MAXN]; // true = 合數,false = 質數
vector<int> primes;

void sieve(int n) {
    is_composite[0] = is_composite[1] = true;
    for (int i = 2; i * i <= n; ++i) {
        if (!is_composite[i]) {
            // 從 i*i 開始(更小的倍數已被更小質數標記)
            for (int j = i * i; j <= n; j += i)
                is_composite[j] = true;
        }
    }
    for (int i = 2; i <= n; ++i)
        if (!is_composite[i])
            primes.push_back(i);
}
💡 從 i*i 開始篩,比 i*i 小的 \(i\) 的倍數已被更小質數標記,不需重複處理
Part 02

線性篩(歐拉篩)

時間複雜度 \(O(n)\),每個合數恰好只被篩一次,是埃篩的進化版本

埃篩的問題

埃篩的核心缺陷:合數會被重複標記

例:12 會被標記兩次
處理質數 2 時:2 × 6 = 12 ✓
處理質數 3 時:3 × 4 = 12 ✓ ← 重複了!
線篩的核心思想
讓每個合數只被它的「最小質因數」篩掉一次,需要在篩的過程中動態維護已找到的質數清單

以 n = 12 為例

primes = []

i = 2:未被標記 → primes = [2]
  j=2: 標記 2×2=4,  2 % 2 == 0 → break

i = 3:未被標記 → primes = [2, 3]
  j=2: 標記 3×2=6,  3 % 2 != 0 → 繼續
  j=3: 標記 3×3=9,  3 % 3 == 0 → break

i = 4:已被標記 → 不加入 primes
  j=2: 標記 4×2=8,  4 % 2 == 0 → break   ← 非質數也要跑!

i = 5:未被標記 → primes = [2, 3, 5]
  j=2: 標記 5×2=10, 5 % 2 != 0 → 繼續
  j=3: 標記 5×3=15, 5 % 3 != 0 → 繼續
  j=5: 標記 5×5=25, 5 % 5 == 0 → break

i = 6:已被標記 → 不加入 primes
  j=2: 標記 6×2=12, 6 % 2 == 0 → break

為什麼 i % prime == 0 要 break?

防止重複標記

假設 i = 6,若不 break,想標記 6 × 3 = 18:

18 = 2 × 9 = 2 × 3²
18 的最小質因數是 2,不是 3
→ 18 應由 i = 9 時,用 9 × 2 來標記 → 現在標記是重複的!

推導

i % prime == 0 時,設 \(i = prime \times k\),下一個想標記的合數:

\[ r = i \times next_{prime} = (prime \times k) \times next_{prime} \]

因為質數遞增,\(prime < next_{prime}\),所以 \(prime\) 才是 \(r\) 真正的最小質因數。 \(r\) 應在未來 \(i' = k \times next_{prime}\) 時被篩掉 → 現在必須 break。

為什麼每個合數恰好被篩一次?

每個合數 \(c\) 都有唯一的最小質因數 \(p\),可以寫成:

\[ c = p \times \underbrace{(c/p)}_{i} \]
歐拉篩保證:當迴圈跑到 \(i = c/p\) 時,\(p\) 一定在 primes 裡,且不會 break 提前結束,所以 \(c\) 只會被標記恰好一次
演算法 時間複雜度 每個合數標記次數
埃拉托斯特尼篩法 \(O(n \log \log n)\) 多次(重複)
線性篩(歐拉篩) \(O(n)\) 恰好一次

線篩程式碼

const int MAXN = 1e7 + 5;
bool is_composite[MAXN];
vector<int> primes;

void linear_sieve(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!is_composite[i])
            primes.push_back(i);     // i 是質數,加入清單

        for (int p : primes) {
            if ((long long)i * p > n) break;  // 超出範圍
            is_composite[i * p] = true;        // 用最小質因數標記

            if (i % p == 0) break;  // ← 核心:確保只篩一次
        }
    }
}
注意 (long long)i * p:當 ip 都很大時,相乘可能造成 int 溢位

番外:莫比烏斯函數篩

下圖是我看到莫比烏斯函數時的表情:

莫比烏斯函數篩 圖片來源:《蒼之彼方的四重奏》
這也是一個從埃篩延伸出來的東西,但應該用不到,所以只是記錄一下有這個東西。

絕對不是因為我看它有點複雜,不想研究

總結

埃拉托斯特尼篩法

  • 逐步篩除質數的倍數
  • 只需篩到 \(\sqrt{n}\)
  • 時間複雜度 \(O(n \log \log n)\)
  • 實作簡單,常用模板

線性篩(歐拉篩)

  • 每個合數只被最小質因數篩一次
  • 動態維護質數清單
  • 時間複雜度 \(O(n)\)
  • 可順便求積性函數(歐拉函數等)