演算法圖鑑
Divide & Conquer · 01 / 04

Master Theorem遞迴式求解

T(n) = aT(n/b) + f(n) 的三種情況

用在:快速判斷分治演算法的複雜度

時間複雜度
空間複雜度
難度進階
前置知識Recursion、Big-O Notation

01為什麼需要它

為什麼切一半就變快

把 n 筆資料排序,兩兩比較要 n² 次。有人說「切成兩半各自排,再合起來」會快很多。但切半也要遞迴再切半,合併還要花 n 的時間,到底快在哪?快多少?

為什麼用它寫成遞迴式 T(n) = 2T(n/2) + n,Master Theorem 直接告訴你答案是 n log n。它讓你看見關鍵:每一層的合併工作加起來剛好是 n,而層數只有 log n。不用每次都手推遞迴樹。

值不值得多切幾份

矩陣乘法切成 4 塊,直覺要做 8 次小乘法。Strassen 想辦法只做 7 次,但多了很多加減法。少一次乘法真的划算嗎?

為什麼用它8T(n/2) + n² 是 Θ(n³),7T(n/2) + n² 是 Θ(n^2.81)。定理告訴你子問題數量 a 決定葉子數 n^(log_b a),而葉子數在這裡壓倒一切,所以多出來的加減法完全不影響結論。

面試時 30 秒內回答複雜度

寫完一個分治或遞迴的解法,面試官問「複雜度多少」。手推遞迴樹又慢又容易錯。

為什麼用它記住三種情況:比較 log_b a 和 d。看一眼遞迴式就能報出 O(n log n)、O(n²) 還是 O(log n),也能解釋為什麼。

看到這些關鍵字就想到它:T(n) = aT(n/b) + f(n)、切成幾份、每份縮小幾倍、遞迴樹、分治的複雜度是多少。

02核心概念

分治演算法的時間都長成同一個形狀:T(n) = a·T(n/b) + f(n)。a 是切成幾個子問題,b 是每個子問題縮小幾倍,f(n) 是切開和合併的成本。這一課只處理 f(n) = Θ(n^d) 的多項式情況,這已經涵蓋絕大多數你會遇到的分治。

想像遞迴樹。第 i 層有 a^i 個子問題,每個大小 n/b^i,所以第 i 層的總工作量是 a^i × (n/b^i)^d = n^d × (a/b^d)^i。這是一個公比 r = a/b^d 的等比數列。等比數列的總和由公比決定:r 大於 1 時最後一項最大,r 等於 1 時每項一樣,r 小於 1 時第一項最大。這就是三種情況的來源。

比較 log_b a(葉子數的指數)和 d(根的工作量指數),等價於比較 r 和 1。情況 1:log_b a > d,葉子主導,T(n) = Θ(n^(log_b a)),例如 Strassen 的 n^2.81。情況 2:log_b a = d,每層工作量都是 n^d,共 log n 層,T(n) = Θ(n^d log n),例如合併排序的 n log n、二分搜尋的 log n。情況 3:log_b a < d,根主導,T(n) = Θ(n^d),例如切半但合併要 n² 的演算法,遞迴根本沒幫上忙。

常見誤區:以為「有切半就是 log n」。切半只保證層數是 log n,總時間還要看每層做多少事。另外定理不涵蓋 f(n) 不是多項式的情況(例如 T(n) = 2T(n/2) + n log n 卡在情況 2 和 3 之間),也不涵蓋子問題不是按比例縮小的情況(例如快速排序最壞情況的 T(n) = T(n−1) + n,每次只少一個),或子問題大小不一樣的情況(例如 T(n) = T(n/3) + T(2n/3) + n)。這些要回頭畫遞迴樹或用代換法。

03演算法步驟

  1. 1從程式碼讀出 a(遞迴呼叫幾次)、b(每次傳入的大小縮小幾倍)、d(遞迴以外的工作是 n 的幾次方)。
  2. 2log_b a。a = 1 時是 0,a = b 時是 1,a = b² 時是 2;其他情況用計算機,例如 log₂ 7 ≈ 2.81。
  3. 3比較 log_b ad。大於:情況 1,答案 Θ(n^(log_b a))。等於:情況 2,答案 Θ(n^d log n)。小於:情況 3,答案 Θ(n^d)。
  4. 4用遞迴樹檢查直覺:算第 0 層、第 1 層、第 2 層的工作量,看是在變大、持平還是變小。方向要和你選的情況一致。
  5. 5若 f(n) 不是純多項式,或子問題大小不均,定理不適用。改畫遞迴樹逐層加總,或猜答案再用歸納法驗證。

04互動示範

選一個常見演算法,或自己組合 a、b、d。上方顯示比較與結論,下方是遞迴樹每一層的工作量:情況 1 的長條逐層變長,情況 2 每層一樣長,情況 3 逐層變短。注意根那層和葉子那層各占總量的比例。

常見演算法
a:切成幾個子問題
b:每個子問題縮小幾倍
d:切與合併的成本 n^d
遞迴式
T(n) = T(n/2) + Θ(n)
葉子數nlog2 2 = n1根的工作f(n) = n1比較log2 2 = 1 = d = 1
情況 2・合併排序
T(n) = Θ(n log n)

a/b^d = 1:每一層工作量都一樣是 n^1,總共 log_b n 層,所以答案是 n^1 × log n。

遞迴樹每層的工作量(n = 1,024第 i 層有 2i 個子問題,每個大小 n/2i,工作量 = n × (2/21)i = n × 1.00i
子問題數 × 大小工作量
0
1 × 1,024
1,024
1
2 × 512
1,024
2
4 × 256
1,024
3
8 × 128
1,024
4
16 × 64
1,024
5
32 × 32
1,024
6
64 × 16
1,024
7
128 × 8
1,024
8
256 × 4
1,024
9
512 × 2
1,024
10(葉)
1,024 × 1
1,024
總和 11,264根那層占 9.1%葉子那層占 9.1%n = 1,024,n log n = 10,240

05程式碼

定理本身不是演算法,這裡的程式碼是一個小計算器:輸入 a、b、d 回傳情況與複雜度,再用遞迴樹逐層加總驗證,看 n 加倍時總工作量放大幾倍。

import math


def npow(d):
    """n^d 的顯示:n^0 寫 1、n^1 寫 n"""
    return "1" if d == 0 else "n" if d == 1 else f"n^{d}"


def master(a, b, d):
    """T(n) = a·T(n/b) + Θ(n^d) 的解。回傳 (情況, 複雜度字串)。
    比較 log_b(a) 與 d:
      大於 → 葉子主導,Θ(n^log_b a)
      等於 → 每層一樣多,Θ(n^d · log n)
      小於 → 根主導,Θ(n^d)
    """
    e = math.log(a, b)                      # 葉子數的指數 log_b a
    if abs(e - d) < 1e-9:
        return 2, "Θ(log n)" if d == 0 else f"Θ({npow(d)} log n)"
    if e > d:
        return 1, f"Θ(n^{e:.2f})"
    return 3, f"Θ({npow(d)})"


# 用遞迴樹逐層加總,驗證定理給的答案
def recursion_tree(a, b, d, n):
    total, size, nodes = 0, n, 1
    while size >= 1:
        total += nodes * size ** d          # 這一層:nodes 個子問題,每個花 size^d
        nodes *= a
        size /= b
    return total


if __name__ == "__main__":
    for name, (a, b, d) in {
        "合併排序": (2, 2, 1),
        "二分搜尋": (1, 2, 0),
        "Karatsuba": (3, 2, 1),
        "Strassen": (7, 2, 2),
    }.items():
        case, ans = master(a, b, d)
        print(f"{name}: T(n) = {'' if a == 1 else a}T(n/{b}) + {npow(d)} → 情況 {case},{ans}")
    # 合併排序: T(n) = 2T(n/2) + n → 情況 2,Θ(n log n)
    # 二分搜尋: T(n) = T(n/2) + 1 → 情況 2,Θ(log n)
    # Karatsuba: T(n) = 3T(n/2) + n → 情況 1,Θ(n^1.58)
    # Strassen: T(n) = 7T(n/2) + n^2 → 情況 1,Θ(n^2.81)

    # n 從 1024 加倍到 2048,看總工作量放大幾倍:
    # 合併排序約 2.2 倍(n log n 在 n 加倍時略多於 2 倍),Strassen 約 7 倍(n^2.81)
    for a, b, d in [(2, 2, 1), (7, 2, 2)]:
        r = recursion_tree(a, b, d, 2048) / recursion_tree(a, b, d, 1024)
        print(f"a={a} b={b} d={d}: n 加倍,工作量 ×{r:.2f}")   # ×2.18、×7.01

06練習題

  • LeetCode 704Binary Search(T(n) = T(n/2) + 1)Easy
  • LeetCode 912Sort an Array(寫合併排序,推 T(n) = 2T(n/2) + n)Medium
  • LeetCode 50Pow(x, n)(T(n) = T(n/2) + 1)Medium
  • LeetCode 241Different Ways to Add Parentheses(子問題不均,定理不適用)Medium
  • LeetCode 932Beautiful Array(T(n) = 2T(n/2) + n)Medium
  • LeetCode 218The Skyline Problem(分治版 T(n) = 2T(n/2) + n)Hard