演算法圖鑑
Math & Number Theory · 02 / 04

Sieve of Eratosthenes質數篩

從小到大劃掉倍數

用在:產生質數表、因數分解

時間複雜度O(n log log n)
空間複雜度O(n)
難度進階
前置知識GCD & LCM、Array & Dynamic Array

01為什麼需要它

產生 RSA 金鑰前先過濾候選數

產生 2048 位元的 RSA 金鑰,要隨機挑大奇數並測試是不是質數。機率質數測試每跑一次都要做上千位元的模冪運算,很貴,而隨機挑到的奇數大多數其實有很小的因數。

為什麼用它OpenSSL 等函式庫內建前幾千個小質數的表,候選數先對這些小質數試除,能被整除的直接丟掉,只有通過的才進入昂貴的機率測試。這張小質數表就是用篩法產生的;大部分候選數在這一關就被刷掉,省下大量白做的模冪運算。

大量整數的質因數分解

資料分析程式要替 100 萬個不超過 10⁷ 的整數計算因數個數,每個數都得先分解質因數。逐一試除到 √x,一個數最壞要做三千多次除法,最壞加起來是數十億次。

為什麼用它先用線性篩在 O(N) 時間內建出「最小質因數表」spf,之後分解 x 只要反覆除以 spf[x],每一步數字至少減半,一個數 O(log x) 步就分解完。上限 10⁷ 的表用 32 位元整數存約 40 MB,換來每次分解只要二十幾步以內。

驗證哥德巴赫猜想到 4 × 10¹⁸

數學家想用電腦驗證「每個大於 2 的偶數都能寫成兩個質數的和」在非常大的範圍內成立,需要一段一段列出 10¹⁸ 附近的所有質數。開一個長度 10¹⁸ 的陣列是不可能的。

為什麼用它區間篩只需要 √R 以內的質數(10⁹ 以內),再對長度幾百萬的區間 [L, R] 劃掉這些質數的倍數,記憶體只和區間長度有關。Oliveira e Silva 等人的驗證計畫正是用分段篩法,一段接一段地掃過整個範圍。

看到這些關鍵字就想到它:需要某個上限內的所有質數、大量查詢一個數是不是質數、大量質因數分解(最小質因數表)、區間 [L, R] 內的質數、上限大約 10⁷ 以內可以開陣列。

02核心概念

判斷一個數是不是質數,最直接的方法是試除到 √x,但要列出 N 以內所有質數時,逐個試除是 O(N√N)。埃拉托斯特尼篩法反過來做:不去檢查每個數,而是讓每個質數去劃掉自己的倍數。一開始把 2 到 N 都當成可能是質數,由小到大掃,遇到第一個還沒被劃掉的數 p,它一定是質數,因為如果它有比自己小的質因數,早就被那個質因數劃掉了;接著把 p 的倍數全部劃掉,再找下一個。

兩個讓它更快的觀察。第一,p 的倍數從 p² 開始劃就好:比 p² 小的倍數是 p·k,其中 k < p,k 的質因數比 p 小,這個數在處理那個更小的質數時已經劃掉了。第二,p² > N 就可以停:任何不超過 N 的合數都能寫成 a·b 且 a ≤ b,於是 a² ≤ N,它一定有一個不超過 √N 的質因數,早就被劃掉了。所以外層迴圈只跑到 √N,之後陣列裡還沒被劃掉的數全是質數。

複雜度:質數 p 要劃大約 N/p 次,總次數是 N/2 + N/3 + N/5 + N/7 + …,而質數倒數的總和只以 log log N 的速度成長,所以時間 O(N log log N)。N = 10⁷ 時 ln ln N 還不到 3,實際上幾乎是線性的。空間是一個長度 N + 1 的布林陣列,O(N)。兩個常用的變形:線性篩讓每個合數只被它的最小質因數劃掉一次,嚴格 O(N),還順便得到最小質因數表,之後分解任何 x ≤ N 都只要 O(log x);區間篩先求出 √R 以內的質數,再用它們劃掉區間 [L, R] 裡的倍數,R 大到 10¹² 也只需要 √R 加上區間長度的記憶體。

常見的坑:0 和 1 忘了設成非質數;陣列開成 N 而不是 N + 1;C++ 裡 p * p 在 N 接近 2³¹ 時會溢位,寫成 p <= n / p;每個質數都從 2p 開始劃雖然正確,但白白多走很多已經劃掉的數;區間篩的起點要取 p² 和「區間內第一個 p 的倍數」兩者較大的,否則 L 很小時會把 p 本身劃掉。和鄰近課程的關係:上一篇 GCD 不需要分解質因數,而篩法正好提供大量分解的工具;下一篇 Modular Arithmetic 常用質數當模數,費馬小定理也要求模數是質數;如果只是要判斷一個 10¹⁸ 等級的數是不是質數,應該用 Miller–Rabin 這類機率測試,而不是篩法。

03演算法步驟

  1. 1開一個長度 N + 1 的布林陣列 is_prime,全部設為 true,再把 0 和 1 設為 false。
  2. 2p 從 2 開始往上走,只要 p² ≤ N 就繼續。
  3. 3is_prime[p] 仍為 true,p 就是質數,把 p², p² + p, p² + 2p, … 不超過 N 的位置全部設為 false;否則直接換下一個 p。
  4. 4p² > N 時停止,陣列裡仍是 true 的位置就是 N 以內的全部質數。
  5. 5要大量分解質因數時改用線性篩記錄最小質因數;上限大到開不了陣列時,只篩到 √R,再對區間 [L, R] 做區間篩。

04互動示範

1 到 60 排成每列 10 個,√60 ≈ 7.75。藍色是目前確認的質數 p,黃色是這一步劃掉的倍數,黃色虛線是早就被更小的質數劃掉、這一輪又走到的數,灰色加刪除線是合數,綠色是確定的質數。p = 2 從 4 開始劃掉全部 29 個偶數;p = 3 從 9 開始,比 9 小的倍數 6 已經被 2 劃掉,這一輪走訪 18 個數、新劃掉 9 個;p = 5 從 25 開始,只新劃掉 25、35、55;p = 7 只剩 49 是新的。下一個沒被劃掉的是 11,但 11² = 121 > 60,所以停止。下方表格記錄每個質數的工作量:總共走訪 57 次、劃掉 42 個合數,其中 15 次是重複劃到,剩下的 17 個數就是 60 以內的質數。

開始N = 60 · √60 ≈ 7.75
1 到 60(每列 10 個)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
目前的質數 pp 的倍數(起點或新劃掉)早就被更小的質數劃掉合數質數
每個質數的工作量
p起點 p²走訪新劃掉
還沒開始劃
未劃掉
59
已劃掉
0
累計走訪
0
步驟 0/10把 2 到 60 全部先當成「可能是質數」。規則只有一條:由小到大找下一個還沒被劃掉的數,它就是質數,再把它的倍數全部劃掉。1 既不是質數也不是合數,不參與。

05程式碼

Python 放標準篩法(用切片一次劃掉整排倍數),以及順便記錄最小質因數的線性篩與查表分解。C++ 放一般篩法和區間篩,示範在 10¹² 到 10¹² + 100 之間找質數:只篩到 √R = 10⁶,陣列長度只有 101。

def sieve(n):
    """埃拉托斯特尼篩法:is_prime[x] 為 True 表示 x 是質數。O(n log log n)"""
    is_prime = [True] * (n + 1)
    is_prime[0:2] = [False] * min(2, n + 1)     # 0 和 1 都不是質數
    p = 2
    while p * p <= n:                           # p² > n 就停:更大的 p 沒有倍數要劃了
        if is_prime[p]:
            # 從 p² 開始劃:比它小的倍數 p·k(k < p)早就被 k 的質因數劃掉
            is_prime[p * p::p] = [False] * ((n - p * p) // p + 1)
        p += 1
    return is_prime


def linear_sieve(n):
    """線性篩:每個合數只被它的最小質因數劃一次。回傳 (質數列表, spf),O(n)"""
    spf = [0] * (n + 1)                         # spf[x]:x 的最小質因數
    primes = []
    for i in range(2, n + 1):
        if spf[i] == 0:                         # 沒有人劃過它,是質數
            spf[i] = i
            primes.append(i)
        for p in primes:
            if p > spf[i] or i * p > n:         # p 一旦超過 i 的最小質因數,i·p 的最小質因數就不是 p
                break
            spf[i * p] = p
    return primes, spf


def factorize(x, spf):
    """查最小質因數表分解質因數,每一步至少除以 2,O(log x)"""
    factors = []
    while x > 1:
        p, cnt = spf[x], 0
        while x % p == 0:
            x //= p
            cnt += 1
        factors.append((p, cnt))
    return factors


if __name__ == "__main__":
    is_prime = sieve(60)
    print([x for x in range(61) if is_prime[x]])
    # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]
    print(sum(sieve(10**6)))                    # 78498:一百萬以內的質數個數
    primes, spf = linear_sieve(100)
    print(len(primes), factorize(84, spf), factorize(97, spf))
    # 25 [(2, 2), (3, 1), (7, 1)] [(97, 1)]

06練習題

  • LeetCode 204Count PrimesMedium
  • LeetCode 2523Closest Prime Numbers in RangeMedium
  • LeetCode 2521Distinct Prime Factors of Product of Array(最小質因數表分解)Medium
  • LeetCode 3233Find the Count of Numbers Which Are Not Special(質數的平方才有恰好兩個真因數)Medium
  • LeetCode 952Largest Component Size by Common Factor(分解質因數後用 Union-Find 合併)Hard
  • LeetCode 2709Greatest Common Divisor TraversalHard