---
# System prepended metadata

title: 質數篩法
tags: [內湖高中程式競賽]

---

# 質數篩法
質數是個非常深奧的數字，在這個單元會討論如何判斷一個正整數是否為質數

## 複習: 什麼是質數?
設兩個數字 $p,q$ 乘起來會變成另一個數 $N$。那麼就可以說 $p,q$ 是 $N$ 的因數。顯然 $N$ 有可能會有很多組因數 $(p,q)$，舉例像是: 
$$
\begin{split}
12&=1\cdot 12\\
&=2\cdot 6\\
&=3\cdot 4\\
\end{split}
$$
這樣我們會說 $12$ 的因數有: $\{1,2,3,4,6,12\}$
透過上面的定義不難觀察到以下性質: 
- 任何正整數 $N$ 的因數肯定有 $1$ 與 $N$
- 除了 $1,N$ 以外的其他因數會被包含在區間 $[2, N-1]$

有些數字是很特殊的存在，他們的因數都只有自己本身與 $1$，像是 $7$ 的因數僅有 $\{1,7\}$。這種數字我們稱為「質數」，因此我們可以觀察到另外一點: 
- 質數在區間 $[2,N-1]$ 不會有任何因數

## 判斷質數
### 暴力作法判斷質數
我們回到我們的問題，如何判斷一個正整數 $N$ 是質數? 透過上面觀察到的性質，不難想出最簡單且直觀的方法，就是遍歷整個區間 $[2, N-1]$，每個數字 $i$ 都除除看，找餘數為何 (在程式中有個很方便的運算子 "%" 可以算餘數)。如果餘數是 $0$ 代表 $N$ 可以整除 $i$，且 $i$ 是 $N$ 的因數

```cpp=
bool isPrime(int N) {
    for (int i = 2; i <= N - 1; i++) { // 檢查整個區間 [2, N - 1]
        if(N % i == 0) {
            return false;
        }
    }
    return true;
}
```
這樣所花的時間顯然為 $O(N-2)=O(N)$。此方法實在太差，或許我們可以考慮縮小區間
### $O(\sqrt N)$ 作法判斷質數
觀察一下等式 $N=p\cdot q$，當 $p$ 能整除 $N$，那 $q$ 當然也能整除 $N$，因此沒必要重複判斷 $q$。反之，如果某整數 $i$ 已經確認不是因數，則 $\tfrac{N}{i}$ 當然也不是因數。再者，因為判斷 $i$ 時已經判斷過 $\tfrac{N}{i}$，所以不需再判斷一次 $\tfrac{N}{i}$。根據等式 $N=p\cdot q$，隨著 $p$ 上升，相對應 $q$ 下降，兩者必然有交點 $p=q=\sqrt{N}$

根據以上，其實只需要判斷到 $[2,\sqrt{N}]$ 就好。我們可以假設要搜尋的整數為 $i$，令搜尋條件為 `i*i<=N`，然後來跑整個迴圈

```cpp=
bool isPrime(int N) {
    for (int i = 2; i * i <= N; i++) {
        if(N % i == 0) {
            return false;
        }
    }
    return true;
}
```
這裡不只用 `i<=sqrt(N)` 的原因是因為可能會有浮點數的問題，這種誤差常常會出問題。在寫程式的時候，浮點數能避開就避，因為很多數字光是儲存就一定會有誤差，更不用說計算時誤差會傳導


## 質數篩法 Sieve of Eratosthenes
這裡講的質數篩法是「埃拉托斯特尼篩法」(埃氏篩法)，也就是古希臘人用的那個方法。假設今天需要判斷的正整數總共有 $q$ 筆資料，那麼時間花費為 $O(q\sqrt{N})$。如果考慮預處理一個表格紀錄「每個數字是否為質數」那麼其實是可以把時間降到 $O(q)$
### 演算法
可以觀察到如果一個數字 $n$ 已經確定是質數，那麼對於所有正整數 $c$，$c\cdot n$ 都是合數，而剩下的就是質數
<center>
<img src="https://upload.wikimedia.org/wikipedia/commons/b/b9/Sieve_of_Eratosthenes_animation.gif" style="margin: 0 auto; display: block; width: 300px">
    <br>
圖取自維基百科
</center>

在實作上，我們需要先建立一個陣列 `is_prime[]` 並開兩個迴圈: 
- 外層找質數
- 內層將該質數的倍數都設為「非質數」

跟上面的方法一樣，外層其實可以只跑區間 $[2, N-1]$ 就好。而內層迴圈中，由於目前跑的數字的平方其實已經標記過了，所以從他開始繼續標記下去
```cpp=
int N;
vector<bool> is_prime(N + 1, true);
is_prime[0] = is_prime[1] = false;

void sieve() {
    for (int i = 2; i * i <= N; i++) {
        if (!is_prime[i]) continue;
        for (int j = i * i; j <= N; j += i)
            is_prime[j] = false;
    }
}
```

### 複雜度分析
- 時間: $O(N\cdot \log \log \sqrt{N})$，推導跟調和級數有關，這邊就不證了
- 空間: $O(N)$

由於`is_prime[]` 是預處理的陣列，一旦建立好就不需要再建一次，所以當我們有 $q$ 比詢問時，時間會是 $O(q)$。要注意每次可以查詢的區間為 $[0, N]$，超過這個區間就沒辦法判斷了。有時候題目可能輸入的是更大的數字時 (例: $10^7$)，時間會爆掉，因此必須考慮其他方法

## 線性篩法
實際在競賽時用埃氏篩法就夠了，根據我的經驗如果再不行就直接砸 Miller-Rabin 演算法，在大多數時候都可以過。但因為我看過這個神奇的線性篩法 (又稱歐拉篩法) 所以還是再多提一點
### 演算法
之前我們的方法是先遍歷整個區間 $[2,N-1]$，找一個質數 $q$，然後篩掉整個區間內所有以 $q$ 為質因數的合數。但是這樣會造成一個問題，就是兩個不同質數可能會篩到同一個數字

我們可以先觀察一個性質: 
- 每個合數 $x$ 都只有一組 $(i, p)$，使得 $x=i\cdot p$，且 $p$ 為 $x$ 最小質因數

根據這個性質，就可以得出以下演算法
```cpp=
vector<int> primes;                  // 紀錄質數有誰
vector<bool> is_prime(N + 5, true);  // 每個數字是否為質數

void linear_sieve() {
    is_prime[0] = is_prime[1] = false;
    for (int i = 2; i <= N; i++) {
        if (is_prime[i]) {           // 還沒被篩掉就是質數
            primes.push_back(i);
        }
        for (int p : primes) {
            if (i * p > N) break;    // 超過就 break
            is_prime[i * p] = false; // 篩掉
            if (i % p == 0) break;   // 下面會講
        }
    }
}
```

### 正確性
在 `if (i % p == 0)` 以前的程式應該都很好懂，但是這個 `if` 判斷就不知道在幹嘛，這很正常。注意到第二層迴圈中，會遍歷目前找到的所有質數 `p`，若是 `break` 代表找到一個質數可以被整除，當然此質數也會是最小。具體來說這件事情可以分兩部分討論: 
- 如果 `i % p == 0`: 這代表 $p$ 就是 $i$ 的最小質因數。$p$ 雖然是 $x = i \times p$ 的最小質因數，但下個質數肯定不是，所以可以 `break`
- 如果 `i % p != 0`: 說明 $p$ 比 $i$ 的任何質因數都小。因此，$p$ 絕對是 $x = i \times p$ 的最小質因數，可以放心標記
### 複雜度分析
因為這方法會跑過所有合數，且不會重複，因此是 $O(N)$
## 應用: 質因數分解
### 預處理: 線性篩
我們可以考慮將最小質因數記錄到 `lp[]` 陣列當中，剩下都一樣，原理剛剛說過了
```cpp=
vector<int> primes;
vector<bool> is_prime(N + 5, true);
vector<int> lp(N + 5, 0);

void linear_sieve() {
    is_prime[0] = is_prime[1] = false;

    for (int i = 2; i <= N; ++i) {
        if (is_prime[i]) {
            lp[i] = i;
            primes.push_back(i);
        }

        for (int p : primes) {
            if (i * p > N)
                break;
            is_prime[i * p] = false;
            lp[i * p] = p;
            if (i % p == 0)
                break;
        }
    }
}
```

### 質因數分解
一個數除以自己當前最小的質因數，直到不能再除就停止。code 還蠻好懂得，就不多做說明
```cpp=
vector<int> factorize(int x) {
    vector<int> fac;
    while (x > 1) {
        fac.push_back(lp[x]);  // 每次提取最小質因數
        x /= lp[x];            // 除掉它，繼續分解剩下的部分
    }
    return fac;
}
```

### 時間複雜度
**線性篩**: $O(N)$
**分解**: 每次 `while` 保守估計會除掉 $2$，因此大概 $O(\log x)$ 次會把 $x$ 降為 $1$ 然後結束迴圈

## 題目練習

[Zerojudge b513. 判斷質數-商競103](https://zerojudge.tw/ShowProblem?problemid=b513)
[Zerojudge a626. 6. Prime Directive](https://zerojudge.tw/ShowProblem?problemid=a626)
[Zerojudge d709. 判断质数（一）](https://zerojudge.tw/ShowProblem?problemid=d709)
[Zerojudge a007. 判斷質數](https://zerojudge.tw/ShowProblem?problemid=a007)
[Zerojudge d362. 10394 - Twin Primes](https://zerojudge.tw/ShowProblem?problemid=d362)
[Zerojudge a010. 因數分解](https://zerojudge.tw/ShowProblem?problemid=a010)
[Zerojudge a537. 10789 - Prime Frequency](https://zerojudge.tw/ShowProblem?problemid=a537)
[Zerojudge a740. 质因数之和](https://zerojudge.tw/ShowProblem?problemid=a740)
[AtCoder Beginner Contest 250D - 250-like Number](https://atcoder.jp/contests/abc250/tasks/abc250_d?lang=en)
[Codeforces 142B. T-primes](https://codeforces.com/problemset/problem/230/B)
[Zerojudge a121. 質數又來囉](https://zerojudge.tw/ShowProblem?problemid=a121)
[Zerojudge d120. 10699 - Count the factors](https://zerojudge.tw/ShowProblem?problemid=d120)
[CSES Common Divisors](https://cses.fi/problemset/task/1081)
[Zerojudge c668. 下一個質數](https://zerojudge.tw/ShowProblem?problemid=c668) (||哈哈||)
[Codeforces 560D. Almost All Divisors](https://codeforces.com/problemset/problem/1165/D)
[Zerojudge e655. 10852 - Less Prime](https://zerojudge.tw/ShowProblem?problemid=e655)
[Codeforces 900F. Vasilije Loves Number Theory](https://codeforces.com/problemset/problem/1878/F)

----

## 參考資料

- [CP Algorithms-Sieve of Eratosthenes](https://cp-algorithms.com/algebra/sieve-of-eratosthenes.html#linear-time-modification)
- [CP Algorithms-Linear Sieve](https://cp-algorithms.com/algebra/prime-sieve-linear.html)
- [OI Wiki-篩法](https://oi-wiki.org/math/number-theory/sieve/)

----

> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2026/1/2