求區間內所有素數是經典算法問題。從最基礎的「埃氏篩」到線性時間複雜度的「歐拉篩(線性篩)」,本文通過代碼演示、步驟推演與實測對比,帶你徹底理解原理與優化細節。
[cpp]
const int MAXN = 100;
bool flag[MAXN];
int primes[MAXN / 3], pi;
void GetPrime_1() {
pi = 0;
memset(flag, false, sizeof(flag));
for (int i = 2; i < MAXN; i++) {
if (!flag[i]) {
primes[pi++] = i; // i 是素數
for (int j = i; j < MAXN; j += i)
flag[j] = true; // 標記 i 的所有倍數
}
}
}
缺陷:合數會被多次標記。例如 10 = 2×5 = 5×2,在 i=2 和 i=5 時各標記一次。時間複雜度約 O(N log log N)。
任意合數 x 可唯一分解為 x = p_min × k,其中 p_min 是 x 的**最小素因子**。若我們只在遍歷到 k 時,用 p_min 去標記 x,則每個合數恰好被標記**一次**。
[cpp]
void GetPrime_2() {
pi = 0;
memset(flag, false, sizeof(flag));
for (int i = 2; i < MAXN; i++) {
if (!flag[i]) primes[pi++] = i; // 發現新素數
for (int j = 0; j < pi && i * primes[j] < MAXN; j++) {
flag[i * primes[j]] = true; // 用已知素數標記合數
if (i % primes[j] == 0) break; // 關鍵剪枝!
}
}
}
if (i % primes[j] == 0) break; 詳細推演以 i = 6 為例,素數表 primes = {2, 3, 5}:
j=0:6×2=12 標記。6 % 2 == 0 → break。j=1 會標記 6×3=18。但 18 = 9×2,最小素因子是 2,應在 i=9 時由 2 標記,此處會重複。規律:當 primes[j] 整除 i 時,primes[j] 是 i 的最小素因子。對於後續更大的素數 primes[k] (k>j),i × primes[k] 的最小素因子仍是 primes[j],應由更小的 i' 配合 primes[j] 來標記。**必須 break 避免重複**。
[cpp]
// 測試 MAXN = 100,000,000 (1億)
clock_t t1 = clock(); GetPrime_1(); printf("普通篩: %d ms\n", clock()-t1);
clock_t t2 = clock(); GetPrime_2(); printf("線性篩: %d ms\n", clock()-t2);
典型輸出:
普通篩: 4200 ms 線性篩: 1050 ms
**效率提升約 4 倍**。隨著 N 增大,優勢更明顯(線性篩嚴格 O(N),普通篩 O(N log log N))。
| 算法 | 核心原理 | 時間複雜度 | 每個合數標記次數 |
|---|---|---|---|
| 普通埃氏篩 | 素數的倍數非素數 | O(N log log N) | 多次(素因子個數) |
| 線性篩 (歐拉篩) | 合數 = 最小素因子 × 另一因子 | O(N) | 恰好 1 次 |
[cpp]
#include <stdio.h>
#include <string.h>
#include <time.h>
const int MAXN = 100000000;
bool flag[MAXN];
int primes[MAXN / 3], pi;
void GetPrime_1() { /* 普通篩實現 */ }
void GetPrime_2() { /* 線性篩實現 */ }
int main() {
printf("測試規模: %d\n", MAXN);
clock_t t1 = clock(); GetPrime_1(); printf("普通篩: %d ms\n", clock()-t1);
clock_t t2 = clock(); GetPrime_2(); printf("線性篩: %d ms\n", clock()-t2);
return 0;
}