演算法概念
分而治之(Divide and Conquer)的三步驟:
- Divide:把大問題切成幾個規模較小、結構相同的子問題。
- Conquer:對每個子問題遞迴求解,小到不能再切時直接解。
- Combine:把子問題的解合起來變成原問題的解。
關鍵是「合起來」——沒有合起來就不是分治,是淘汰。淘汰切完只進其中一塊、其他丟掉;分治切完每塊都要解,最後再合回去。
典型遞迴式 : 是子問題個數、 是子問題規模相對原問題縮幾倍、 是合併成本。Combine 決定整體效率——合併做到 就能配上 ,合併若變成 整體就退化到 。
適合分治的條件:子問題之間獨立(不獨立就要動態規劃)、合併成本合理(通常要 或更低)。
各適用情境
合併排序(Merge Sort)
演算法策略
Divide:從中間切成左右兩半。Conquer:各自遞迴排序。Combine:兩個已排序的小陣列用雙指標合併——各指一個最左元素,誰小先寫進結果,相等時優先取左邊(保穩定性)。
複雜度
每層合併 、共 層 → 。不論輸入如何都這麼快。代價是要 額外空間。
範例
題目:[38, 27, 43, 3, 9, 82, 10] 從小到大排序。
圖解:
步驟:
Divide 階段:一路切半到單元素。
| 層 | 陣列狀態 |
|---|---|
| 0 | [38, 27, 43, 3, 9, 82, 10] |
| 1 | [38, 27, 43, 3] | [9, 82, 10] |
| 2 | [38, 27] | [43, 3] | [9] | [82, 10] |
| 3 | [38] | [27] | [43] | [3] | [9] | [82] | [10] |
Combine 階段:由下往上兩兩合併。
第 3 → 2 層:
| 合併 | 結果 |
|---|---|
[38] + [27] | [27, 38] |
[43] + [3] | [3, 43] |
[9] 落單 | [9] |
[82] + [10] | [10, 82] |
第 2 → 1 層:
| 合併 | 結果 |
|---|---|
[27, 38] + [3, 43] | [3, 27, 38, 43] |
[9] + [10, 82] | [9, 10, 82] |
第 1 → 0 層:合併 [3, 27, 38, 43] + [9, 10, 82],雙指標細節:
| 取 | |||||
|---|---|---|---|---|---|
| 0 | 0 | 3 | 9 | [3] | |
| 1 | 0 | 27 | 9 | [3, 9] | |
| 1 | 1 | 27 | 10 | [3, 9, 10] | |
| 1 | 2 | 27 | 82 | [3, 9, 10, 27] | |
| 2 | 2 | 38 | 82 | [3, 9, 10, 27, 38] | |
| 3 | 2 | 43 | 82 | [3, 9, 10, 27, 38, 43] | |
| 4 | 2 | — | 82 | 用完、接 | [3, 9, 10, 27, 38, 43, 82] |
Python 實作
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
return merge(merge_sort(arr[:mid]), merge_sort(arr[mid:]))
def merge(left, right):
result, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
return result + left[i:] + right[j:]快速排序(Quick Sort)
演算法策略
跟合併排序是分治的鏡像:Divide 昂貴(挑 pivot 後原地用雙指標重排、左邊都 pivot、右邊都 pivot),Combine 便宜(什麼都不做——原地分好就排好了)。
Hoare partition 的流程(pivot = a[left]):
- 左指標 :從
left+1起往右,停在a[i] >= pivot的位置。 - 右指標 :從
right起往左,停在a[j] <= pivot的位置。 - 如果
i < j就 swap(兩個站錯邊);如果i >= j就跳出外層、做swap(a[left], a[j]),pivot 就定位。
複雜度
平均 pivot 切出接近均等的兩半 → 。最壞 pivot 每次都極值、一邊空一邊 → 。實務用隨機 pivot 或三數取中(median-of-three)避免 worst case。
範例
題目:[25, 10, 35, 5, 30, 15](1-indexed)排序。
步驟:
第一層 Quick_Sort(1, 6):pivot = a[1] = 25,、。
| 輪 | i 往右掃停在 | j 往左掃停在 | i < j? | 動作 | 陣列 |
|---|---|---|---|---|---|
| 1 | () | () | 是 | swap | [25, 10, 15, 5, 30, 35] |
| 2 | () | () | 否 | 跳出外層 | [25, 10, 15, 5, 30, 35] |
跳出後 swap(a[1], a[4]) → [5, 10, 15, 25, 30, 35]。pivot 25 落在位置 4。左半 (1, 3)、右半 (5, 6)。
第二層左 Quick_Sort(1, 3):[5, 10, 15],pivot = 5。 停在 2、 走到 1,swap(a[1], a[1]) 不動。
第三層 Quick_Sort(2, 3):[10, 15],pivot = 10。 停在 3、 走到 2,swap(a[2], a[2]) 不動。
第二層右 Quick_Sort(5, 6):[30, 35],pivot = 30,同理不動。
最終 [5, 10, 15, 25, 30, 35]。整個過程沒有 Combine——partition 原地分、遞迴回來就排好。
另一種看法:每次 partition 完成的快照
上面那張表追的是 partition 內部的 i/j 小輪。換個粒度——只記每次 partition 完成、pivot 落定的瞬間:
| 粒度 | 是什麼 |
|---|---|
| 小輪 | i/j 各停一次、要嘛 swap 要嘛跳出(對應上面「輪」表的每一列) |
| 一次 partition | 從進入 Quick_Sort 到 pivot 落定(對應下面全程表的每一列) |
下表【 】框住待處理子陣列、left/right 指下一次 partition 的範圍:
| left | right | ||||||
|---|---|---|---|---|---|---|---|
| 【25 | 10 | 35 | 5 | 30 | 15】 | 1 | 6 |
| 【5 | 10 | 15】 | 25 | 【30 | 35】 | 1 | 3 |
| 5 | 【10 | 15】 | 25 | 【30 | 35】 | 2 | 3 |
| 5 | 10 | 15 | 25 | 【30 | 35】 | 5 | 6 |
| 5 | 10 | 15 | 25 | 30 | 35 | — | — |
每一列到下一列就是一次完整 partition(內部小輪都被吞進去)。pivot 落定後就不再動,剩下的子陣列各自遞迴;單元素子陣列直接回傳、不畫列。
Python 實作(Hoare partition)
def quick_sort(a, left, right):
if left >= right:
return
pivot = a[left]
i, j = left, right + 1
while True:
while True:
i += 1
if i > right or a[i] >= pivot:
break
while True:
j -= 1
if a[j] <= pivot:
break
if i < j:
a[i], a[j] = a[j], a[i]
else:
break
a[left], a[j] = a[j], a[left]
quick_sort(a, left, j - 1)
quick_sort(a, j + 1, right)例題:Quick Sort worst case
已排序 [1, 2, 3, 4, 5]、固定選 a[left]:每次 pivot 都是最小值,左半空、右半 個。。實務用隨機 pivot 或 median-of-three 避免。
二維平面上的極點
演算法策略
極點(maximal point):若 且 ,則 宰制 。沒被任何人宰制的點就是極點。所有極點排起來是一條「往右下的階梯」。
分治招式:
- Divide:按 x 中位數切成左半 (小 x)、右半 (大 x)。
- Conquer:各自遞迴求 、。
- Combine:
- 的每點 x 都大於 全部 → 不可能被 宰制 → 全保留。
- 裡的點要跟 比 y——令 (就是 的最大 y), 裡 保留、 丟掉。
為什麼只比 的最大 y 就夠?因為 裡最大 y 的那個點本身一定是極點,它能宰制的範圍就是 裡最廣的——它都打不贏的 點,其他 點也打不贏。
極點跟凸包不是同一回事:凸包關心「最外圍輪廓」(上下左右都算)、極點只關心「右上角」。
複雜度
每層合併掃一遍 、共 層 → 。若輸入未排序,初始按 x 排序也是 。
範例
題目:8 個點(已按 x 排序)。
圖解:
步驟:中位 x 切半,、。
遞迴 :切成 和 。
| 子問題 | 左邊極點 | 右邊 | 合併 |
|---|---|---|---|
| 8() | 丟 → | ||
| 7() | 丟 → |
合併:右半 的 ,左半 中 保留 → 。
遞迴 :同理求得 。
最後合併: 最大 y = 5()。 中 、 都保留。最終極點 。座標 ,y 遞減階梯。
Python 實作
def maximal_points(points):
pts = sorted(points, key=lambda p: p[0])
def solve(a):
if len(a) == 1:
return a
mid = len(a) // 2
L = solve(a[:mid])
R = solve(a[mid:])
y_star = max(p[1] for p in R)
L_keep = [p for p in L if p[1] > y_star]
return L_keep + R
return solve(pts)例題:極點數量的極端情況
(a) 所有點都是極點:x 遞增時 y 嚴格遞減(左上到右下的階梯)。任兩點 、 若 則 ,無人能同時在 x、y 兩維都贏 → 無人被宰制。極點數達上限 。
(b) 極點最少:x 遞增時 y 嚴格遞增(左下到右上的線)。除最右上那個,任一點都被下一個宰制。極點只有最右上那個。
(c) 下界 = 1:x 最大的那個點永遠無人能在 x 上贏過它 → 必為極點 → 極點數 。結合 (b),最少恰為 1。
整理:。隨機分佈下期望極點數為 。
最接近的兩點(Closest Pair)
演算法策略
平面 點,找距離最近的一對。Naive 兩兩配對 。分治能壓到 ——關鍵在 Combine 的巧思。
- 預先按 x 排序。
- Divide:中位 x 切左右半 、。
- Conquer:、、。
- Combine:檢查跨越中垂線的點對。
Combine 若兩兩配對會爆成 ,得想辦法剪枝。
Strip(中垂線帶):以中垂線 為中心、左右各 寬的直立帶子,寬度 ;形式定義 。
關鍵直覺:離中垂線超過 的點,光 x 軸距離就 ,不可能和任何另一點配出 的距離——整個跳過。所以只用 strip 內的點繼續配對。
strip 裡的點按 y 排序後,每個點只需往下檢查最多 7 個就夠——strip 寬 、左右兩側內部最近距離都 ,幾何上 的矩形至多容 8 個點。下方範例的 ClosestPairDiagram 從第 5 步起會把 strip(藍色帶狀區)疊在中垂線兩側。
複雜度
每層 Combine (strip 篩 + y 排序 + 每點掃 ≤ 7)、共 層 → 。實作技巧:遞迴過程中同時維護 x 和 y 兩份預排序索引,避免每層重排 strip 退化成 。
範例
題目:6 個點 。
圖解:
步驟:
Divide:中位 x ≈ 4,左半 [A, B, C]、右半 [D, E, F]。
左半(3 點直接兩兩算):
| 對 | 距離 |
|---|---|
。
右半:
| 對 | 距離 |
|---|---|
。。
Combine:中垂線 ,strip 收 ,即 → 。按 y 排序:。
每個點往下檢查 y 差 < 2.24 的:
| 起點 | 檢查 | 距離 | 更新? | |
|---|---|---|---|---|
| 1 | 否 | |||
| 3 > 2.24 | 跳 | — | ||
| 2 | 否 | |||
| 3 > 2.24 | 跳 | — | ||
| 1 | 否 |
沒更短的 → 最近距離 ( 或 )。
Python 實作
import math
def closest_pair(points):
pts = sorted(points, key=lambda p: p[0])
def dist(p, q):
return math.hypot(p[0] - q[0], p[1] - q[1])
def solve(a):
if len(a) <= 3:
return min((dist(a[i], a[j]) for i in range(len(a)) for j in range(i + 1, len(a))), default=float('inf'))
mid = len(a) // 2
xm = a[mid][0]
d = min(solve(a[:mid]), solve(a[mid:]))
strip = sorted([p for p in a if abs(p[0] - xm) < d], key=lambda p: p[1])
for i in range(len(strip)):
for j in range(i + 1, min(i + 8, len(strip))):
if strip[j][1] - strip[i][1] >= d:
break
d = min(d, dist(strip[i], strip[j]))
return d
return solve(pts)快速傅立葉轉換(FFT)
演算法策略
DFT(Discrete Fourier Transform,離散傅立葉轉換):把長度 的序列 換成另一串 。沒新增、沒減少資訊,只是同一筆資料換個視角看(小寫 是輸入、大寫 是轉換後輸出,下同)。FFT(Fast Fourier Transform)是 DFT 的快速計算法,把時間從 壓到 。
最經典的應用是多項式乘法:兩個 次多項式直接相乘 ;若先各自 FFT 變成「點值表示」,乘法就退化成點對點相乘 ,再用反向 FFT(IFFT)轉回係數,總共 ,比直接乘快很多。
兩個小道具: 與
是虛數單位,定義 ;自乘四次轉一圈:
| 次方 | |||||
|---|---|---|---|---|---|
| 值 | (轉回) |
是「 次單位根」(-th root of unity),FFT/DFT 挑來代入計算的特殊複數,定義 。 時 , 表跟 表幾乎一樣(差個負號):
| 次方 | |||||
|---|---|---|---|---|---|
| 值 | (轉回) |
關鍵性質: 自乘 次就轉一圈回到 ,所以指數對 取餘數即可查表(、⋯⋯)。
DFT 公式:
拆白話:
- (output index):第幾個輸出,共 個 各跑一次。
- (input index):輸入編號,把 各乘一個 再加起來。
- : 對 取餘後查上表。
每個 就是「把所有 各乘一個從表查到的數,全部加起來」。
FFT 的加速觀察:DFT 公式直接展開要 。把奇偶下標拆兩組會出現重複計算,分治下去得 。具體流程是 Cooley–Tukey butterfly 遞迴,手算考題很少要求展開到這層;考試多半要的是用 DFT 公式硬代,FFT 是程式實作時才寫的。
複雜度
| 算法 | 時間 |
|---|---|
| DFT(直接套公式) | |
| FFT | |
| 多項式乘法(FFT + 點對點 + IFFT) |
多項式乘法的整體流程:
| 階段 | 動作 | 成本 |
|---|---|---|
| 1. 係數 → 點值 | 對 、 各做 FFT 得 、 | |
| 2. 點值乘法 | 點對點相乘 | |
| 3. 點值 → 係數 | 對 做 IFFT 得 |
關鍵就是「換視角後乘法變便宜」——直接係數相乘 ,繞點值一圈反而 。
範例
題目:對 做 DFT,。
步驟:先記好 表 ,然後逐個 套公式。
():每項都乘 ,全部加起來。
():
| 0 | 0 | 0 | |
| 1 | 1 | 1 | |
| 2 | 2 | 2 | |
| 3 | 3 | 3 |
():,對 取餘 → 。
| 0 | 0 | 0 | |
| 1 | 1 | 2 | |
| 2 | 2 | 0 | |
| 3 | 3 | 2 |
():,對 取餘 → 。
| 0 | 0 | 0 | |
| 1 | 1 | 3 | |
| 2 | 2 | 2 | |
| 3 | 3 | 1 |
結果:
Python 實作(用 numpy)
import numpy as np
def poly_multiply(A, B):
n = 1
while n < len(A) + len(B) - 1:
n *= 2
A_hat = np.fft.fft(A, n)
B_hat = np.fft.fft(B, n)
C = np.fft.ifft(A_hat * B_hat).real
return [round(x) for x in C[:len(A) + len(B) - 1]]
print(poly_multiply([1, 2, 3], [4, 5])) # [4, 13, 22, 15]FFT 派得上用場的地方
| 問題 | 不用 FFT | 用 FFT |
|---|---|---|
| 多項式乘法 | ||
| 大整數乘法 | ||
| 訊號卷積 / 濾波 |
只要看到「兩個序列配對累加」(卷積),FFT 幾乎都能套。Python 的 numpy.fft、scipy.signal.fftconvolve、MP3/JPEG 壓縮都用它。
常見考法
| 題型 | 必備技能 |
|---|---|
| 合併排序手跑 | 畫遞迴樹、列每輪 merge 後陣列;雙指標合併 |
| 快速排序手跑 | Hoare partition(pivot = a[left]、i/j 雙指標掃到交錯、swap a[left] ↔ a[j]) |
| Quick Sort worst case | 已排序 + 固定選首 pivot → |
| 極點 | 按 x 排序切半、 最大 y 當門檻過濾 |
| 最接近兩點 | strip 寬 、每點只檢查 y 接近的 7 個 → 合併 |
| FFT | 記住 、適用卷積 / 多項式乘法 |
| 主定理 | → → 比 與 → Case 1/2/3 |
| 展開法推導 | 展 2–3 層看規律、找觸底 、代回求和 |
常見雷:
- Hoare partition 要先
i++再比較(不是先比較再i++);結束條件i >= j不是i > j。 - 極點 凸包。凸包關心「最外圍輪廓」,極點只關心「右上角」。
- Combine 若做到 、整體變 (主定理 Case 3 以外)。
下一篇動態規劃——當子問題不再獨立、彼此重疊時,分治會重複計算,DP 用「記下來不再重算」解掉這個低效。
Latest Updates
- 2026.04.10 Content updated
