# 質數篩法
質數是個非常深奧的數字,在這個單元會討論如何判斷一個正整數是否為質數
## 複習: 什麼是質數?
設兩個數字 $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