演算法概念
每次比較或測試,都讓一整塊搜尋空間被證明「不可能含答案」,就整塊丟掉、只在剩下的地方繼續找。丟掉的不再回頭——這就是淘汰與搜尋。能用這個招式的關鍵,是你要有一個便宜( 或 )的測試,一次就能把問題規模砍成 。砍得越狠、每輪越便宜,整體就越快。
一次比較能產生幾種互斥結果,就決定了底數 :是非題切兩半、三值天平切三份。選擇不是隨便訂的,是被工具解析度逼出來的。
各適用情境
偽幣問題
演算法策略
一個無砝碼天平、 枚硬幣裡藏一枚已知較輕的偽幣。把硬幣分成三等份——左盤、右盤、未上秤。秤一次看三個結果:左輕、右輕、平衡,偽幣就在對應那一組。對那組再做同樣的事,直到剩一枚。
為什麼分三組不分兩組?因為天平一次吐三個結果,天生就切三份;硬要切兩份是浪費資訊量。
複雜度
每輪秤一次(),規模砍成三分之一。總共 。如果工具只能回答是/否,那就變 。
範例
題目:9 枚硬幣編號 0–8,偽幣是 5,用最少次數找出來。
圖解:
步驟:
| 回合 | 左盤 | 右盤 | 未上秤 | 結果 | 剩下候選 |
|---|---|---|---|---|---|
| 1 | {0,1,2} | {3,4,5} | {6,7,8} | 右盤輕 | {3,4,5} |
| 2 | {3} | {4} | {5} | 平衡(左右都真) | {5} |
兩次秤量命中。
Python 實作
def find_fake_light(coins):
if len(coins) == 1:
return coins[0]
k = len(coins) // 3
left, right, rest = coins[:k], coins[k:2*k], coins[2*k:]
ls, rs = sum(left), sum(right)
if len(left) == len(right):
if ls < rs:
return find_fake_light(left)
if ls > rs:
return find_fake_light(right)
return find_fake_light(rest)
return find_fake_light(left + right + rest)例題:方向未知的偽幣(4 枚未知 + 參考真幣,兩次秤量)
兩組各 4 枚硬幣:
- :恰有一枚偽幣,不知道偏輕或偏重
- :全是真幣,可作為參考
用天平兩次找出偽幣並判斷輕重。
資訊量檢查: 種情況, 種結果路徑, 足夠。
第一次秤量 : 對
| 結果 | 推論 | 偽幣候選 | 方向 |
|---|---|---|---|
| 平衡 | 都是真幣 | 未知 | |
| 左重 | 中有一枚重偽幣 | 重 | |
| 左輕 | 中有一枚輕偽幣 | 輕 |
第二次秤量 :依 結果決定
- 平衡 → : 對 。左重 → 重;左輕 → 輕。
- 左重 → : 對 。左重 → 重;右重 → 重;平衡 → 重。
- 左輕 → : 對 。左重 → 輕;左輕 → 輕;平衡 → 輕。
正確性驗證(8 種情況全覆蓋):
| 偽幣 | 方向 | 結果 | 判定 | ||
|---|---|---|---|---|---|
| A | 重 | 左重 | A vs B 左重 | A 重 ✓ | |
| A | 輕 | 左輕 | A vs B 左輕 | A 輕 ✓ | |
| B | 重 | 左重 | A vs B 右重 | B 重 ✓ | |
| B | 輕 | 左輕 | A vs B 左重 | B 輕 ✓ | |
| C | 重 | 左重 | A vs B 平衡 | C 重 ✓ | |
| C | 輕 | 左輕 | A vs B 平衡 | C 輕 ✓ | |
| D | 重 | 平衡 | D vs T 左重 | D 重 ✓ | |
| D | 輕 | 平衡 | D vs T 左輕 | D 輕 ✓ |
關鍵技巧:用參考真幣壓住不確定性。第一次秤把「偽幣身分」與「偏向」耦合進同一個結果——平衡代表偽幣是 (方向待查)、不平衡代表方向已定、只剩 3 枚嫌疑犯。第二次秤量剛好各處理 3 種結果,資訊量完美耗盡。
若沒有參考真幣,4 枚未知方向的硬幣無法用兩次秤量同時找出偽幣與方向——任何第一次秤量都會把 8 種情況壓成 3 種結果,必有一支裝超過 3 種,第二次切不開。原始 12 硬幣問題(無參考真幣)需要 3 次秤量。
例題:一般公式
次秤量可解的硬幣數:
- 已知方向: 枚
- 未知方向(無參考真幣): 枚
- 未知方向(有參考真幣): 枚
常考:、未知方向、無參考真幣 → 枚。
晶片測試
演算法策略
個晶片,好晶片嚴格多於一半。兩個晶片互測時:兩好互報「好」、一好一壞時好的誠實、兩壞隨便回。我們兩兩配對,若兩邊都說對方好就保留其中一個(另一個丟掉);只要有人說對方壞就兩個都丟。一輪下來,剩下的集合裡好晶片還是嚴格多數——遞迴做就行。
關鍵觀察:都說對方好的兩片必為同類(同好或同壞),不可能一好一壞,因為好的會揭發壞的。
複雜度
每輪配對掃一遍要 ,規模減半:。線性次測試就能找到一片保證的好晶片。
範例
題目: 晶片,其中 5 個好、3 個壞,找出一片好的。
步驟(條列比表格清楚):
- 兩兩配對:(1,2)(3,4)(5,6)(7,8)。
- 測試結果假設:
- (1,2) 互報好 → 保留 1,丟 2
- (3,4) 互報好 → 保留 3,丟 4
- (5,6) 有人說對方壞 → 全丟
- (7,8) 互報好 → 保留 7,丟 8
- 剩下 ,好晶片仍嚴格多於一半。
- 繼續配對 (1,3)、7 落單 → 測試、再篩選,直到剩一片。
Python 實作
def chip_test(chips, is_good):
while len(chips) > 1:
kept = []
i = 0
while i + 1 < len(chips):
a, b = chips[i], chips[i + 1]
a_says = is_good(a, b)
b_says = is_good(b, a)
if a_says and b_says:
kept.append(a)
i += 2
if len(chips) % 2 == 1:
kept.append(chips[-1])
chips = kept
return chips[0]二元搜尋
演算法策略
陣列已排序,中間那個值跟目標比一下:相等就中獎;目標較大就丟掉中點以左;較小就丟掉中點以右。每次砍半。
複雜度
每輪一次比較、規模減半:。
範例
題目:陣列 ,找 。
圖解:
步驟:
| 回合 | lo | hi | mid | 動作 | |
|---|---|---|---|---|---|
| 1 | 0 | 11 | 5 | 31 | → 淘汰左半,lo ← 6 |
| 2 | 6 | 11 | 8 | 64 | → 淘汰右半,hi ← 7 |
| 3 | 6 | 7 | 6 | 42 | → 淘汰左半,lo ← 7 |
| 4 | 7 | 7 | 7 | 56 | 命中,回傳索引 7 |
Python 實作
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1例題:1-indexed、上取整、目標不存在
有些課本採 1-indexed 並用 取中點(右偏中點)。邏輯完全一致,差別只在索引起點與偶數長度的中點位置。
陣列 ,找 。
| 回合 | left | right | middle | 比較 | 動作 | |
|---|---|---|---|---|---|---|
| 1 | 1 | 18 | 10 | 18 | left ← 11 | |
| 2 | 11 | 18 | 15 | 36 | right ← 14 | |
| 3 | 11 | 14 | 13 | 30 | left ← 14 | |
| 4 | 14 | 14 | 14 | 35 | right ← 13 |
第 4 回合後 left = 14 > right = 13,結束—— 不在陣列中。 夾在 與 之間。
選出第 小的元素
演算法策略
選一個 pivot 把陣列切成「比 pivot 小」「等於 pivot」「比 pivot 大」三堆。看 落在哪一堆,就只遞迴那一堆,另外兩堆整個丟掉。兩種變體差在 pivot 怎麼挑:
- BFPRT(Median of Medians):每 5 個一組取中位數,再對這些中位數遞迴求中位數當 pivot。稍慢但保證每輪砍掉至少 ,最差還是 。
- Randomized Select:隨手抽一個當 pivot。期望 、運氣差時會退化成 (機率極低)。
複雜度
- BFPRT:。
- :對 個中位數遞迴求 MoM
- :每輪保證砍掉 ,剩下
- :分組、取中位數、分堆都是線性
- Randomized Select:期望 、最差 。
為什麼每組 5 個?組大小 3 時 不收斂;組大小 5 是「最小可線性」的選擇;組大小 7 常數更大不划算。
範例
題目:,用 BFPRT 找第 8 小。
圖解:
步驟:
第一輪:25 個元素,分 5 組取中位數:
| 組 | 原始 | 排序後 | 中位數 |
|---|---|---|---|
| G1 | 17, 3, 25, 8, 41 | 3, 8, 17, 25, 41 | 17 |
| G2 | 12, 36, 5, 29, 14 | 5, 12, 14, 29, 36 | 14 |
| G3 | 22, 7, 33, 19, 46 | 7, 19, 22, 33, 46 | 22 |
| G4 | 2, 38, 11, 27, 6 | 2, 6, 11, 27, 38 | 11 |
| G5 | 43, 15, 31, 9, 21 | 9, 15, 21, 31, 43 | 21 |
中位數集 排序後 ,MoM = 17 → pivot。
以 pivot = 17 分堆:
- (< 17):11 個()
- (= 17):1 個
- (> 17):13 個
→ 到 找第 8 小。
第二輪: 共 11 個:
| 組 | 原始 | 排序後 | 中位數 |
|---|---|---|---|
| G1 | 3, 8, 12, 5, 14 | 3, 5, 8, 12, 14 | 8 |
| G2 | 7, 2, 11, 6, 15 | 2, 6, 7, 11, 15 | 7 |
| G3 | 9 | 9 | 9 |
MoM = 8 → pivot。
以 8 分堆:(5 個)、(1 個)、(5 個)。 → 到 找第 小。
第三輪: 共 5 個:排序後 ,MoM = 12 → pivot。
分堆:(2 個)、、。 → 到 找第 2 小。
第四輪: 共 2 個: 排序後 ,第 2 小 = 11。
驗證:原陣列排序後 ,第 8 位確實是 11 ✓
Python 實作(BFPRT + Randomized Select)
def median_of_medians(arr, k):
if len(arr) <= 5:
return sorted(arr)[k - 1]
groups = [sorted(arr[i:i + 5]) for i in range(0, len(arr), 5)]
medians = [g[len(g) // 2] for g in groups]
pivot = median_of_medians(medians, len(medians) // 2 + 1)
L = [x for x in arr if x < pivot]
E = [x for x in arr if x == pivot]
G = [x for x in arr if x > pivot]
if k <= len(L):
return median_of_medians(L, k)
if k <= len(L) + len(E):
return pivot
return median_of_medians(G, k - len(L) - len(E))
import random
def randomized_select(arr, k):
if len(arr) == 1:
return arr[0]
pivot = random.choice(arr)
L = [x for x in arr if x < pivot]
E = [x for x in arr if x == pivot]
G = [x for x in arr if x > pivot]
if k <= len(L):
return randomized_select(L, k)
elif k <= len(L) + len(E):
return pivot
return randomized_select(G, k - len(L) - len(E))例題:27 個數字找第 16 小(pivot 剛好命中 )
陣列 ,。
分 6 組(前 5 組 5 個、最後一組 2 個):
| 組 | 原始 | 排序後 | 中位數 |
|---|---|---|---|
| G1 | 7, 3, 6, 8, 1 | 1, 3, 6, 7, 8 | 6 |
| G2 | 10, 4, 3, 8, 5 | 3, 4, 5, 8, 10 | 5 |
| G3 | 7, 10, 0, 10, 2 | 0, 2, 7, 10, 10 | 7 |
| G4 | 1, 4, 4, 13, 5 | 1, 4, 4, 5, 13 | 4 |
| G5 | 0, 8, 9, 9, 2 | 0, 2, 8, 9, 9 | 8 |
| G6 | 6, 1 | 1, 6 | 6 |
中位數集 排序後 ,MoM = 6 → pivot。
以 6 分堆:
| 堆 | 內容 | 大小 |
|---|---|---|
| (< 6) | 3, 1, 4, 3, 5, 0, 2, 1, 4, 4, 5, 0, 2, 1 | 14 |
| (= 6) | 6, 6 | 2 |
| (> 6) | 7, 8, 10, 8, 7, 10, 10, 13, 8, 9, 9 | 11 |
→ 答案就是 pivot = 6,不用再遞迴。
常見錯誤:G4 的中位數誤寫成 5、G5 誤寫成 6。一定要先排序再取中間,不是「原始順序的第 3 個」。
例題:Randomized Select 同一組找第 8 小
假設 pivot 剛好依序抽到 21 → 9 → 12。
第一輪:pivot = 21。 共 13 個、 1 個、 11 個。 → 進 。
第二輪:,pivot = 9。(6 個)、、(6 個)。 → 進 找第 小。
第三輪:pivot = 12。、、。 → 進 。
第四輪:,直接回傳 11。
與 BFPRT 答案一致。差別:BFPRT pivot 固定為 MoM,穩但慢;Randomized 隨手抽,快但最差 。
打包問題(Bin Packing)
演算法策略
個物品、大小 ,箱容量 ,目標用最少箱子裝完。這題是 NP-Hard,實務用近似演算法:
- Next Fit (NF):只看最後一個箱子,塞不下就開新箱。
- First Fit (FF):從第一個箱子掃,第一個塞得下的就放。
- Best Fit (BF):選剩餘空間最少且還裝得下的箱子。
- First Fit Decreasing (FFD):先把物品由大到小排序,再跑 FF。
直覺:先放大的物品、小的用來填縫,所以 FFD 最好。要精確最佳解時用 Branch and Bound——對決策樹上下界不可能比目前最佳解好的子樹整個剪掉,這就是淘汰思想。
複雜度
- NF / FF / BF:(找可用箱子要 ,總共 個物品)
- FFD:(排序主導)
近似比(OPT 為最佳解箱子數):
| 策略 | 近似比 |
|---|---|
| Next Fit | |
| First Fit | |
| First Fit Decreasing |
WG(Wasted Gap)= 箱子關閉時的剩餘容量;WE(Wasted Element)= 被所有現有箱子拒絕而被迫開新箱的物品。WG 累積越少、WE 越少發生,近似比就越好。
範例
題目:物品 ,箱容量 ,跑 FFD。
步驟:排序後由大到小 。
| 步 | 物品 | 動作 | 箱況(剩餘容量) | WE |
|---|---|---|---|---|
| 1 | 0.7 | 開 Bin1 | Bin1: 0.3 | — |
| 2 | 0.5 | Bin1(0.3) 塞不下 → 開 Bin2 | Bin1: 0.3, Bin2: 0.5 | +1 |
| 3 | 0.5 | Bin1 不行、Bin2 剛好 | Bin1: 0.3, Bin2: 0.0 | — |
| 4 | 0.4 | Bin1 不行、Bin2 不行 → 開 Bin3 | Bin1: 0.3, Bin2: 0.0, Bin3: 0.6 | +1 |
| 5 | 0.3 | Bin1 剛好 | Bin1: 0.0, Bin2: 0.0, Bin3: 0.6 | — |
| 6 | 0.2 | Bin3 可塞 | Bin3: 0.4 | — |
| 7 | 0.2 | Bin3 可塞 | Bin3: 0.2 | — |
| 8 | 0.1 | Bin3 可塞 | Bin3: 0.1 | — |
最終:3 個箱子、總 WG = 0.1。物品總大小 2.9、下界 → FFD 打到最佳。
Python 實作(NF、FF、FFD)
def next_fit(items, C):
bins = [0]
for x in items:
if bins[-1] + x > C:
bins.append(x)
else:
bins[-1] += x
return bins
def first_fit(items, C):
bins = []
for x in items:
placed = False
for i, used in enumerate(bins):
if used + x <= C:
bins[i] += x
placed = True
break
if not placed:
bins.append(x)
return bins
def first_fit_decreasing(items, C):
return first_fit(sorted(items, reverse=True), C)例題:NF vs FF vs FFD 比較
物品 、:
- NF:0.5→Bin1(剩 0.5);0.7→Bin1 塞不下開 Bin2(剩 0.3);0.5→Bin2 塞不下開 Bin3(剩 0.5);… 最終約 4~5 箱,浪費最多。
- FF:不排序、照原順序從頭找可用箱。大致 3~4 箱。
- FFD:如上例,3 箱,最佳。
結論:FFD 在這組資料上是最佳,也符合近似比最緊的直覺。
常見考法
| 題型 | 必備技能 |
|---|---|
| 偽幣(已知方向) | 三分法,; 次可解 枚 |
| 偽幣(方向未知) | (無參考)、(有參考);、12 枚是經典 |
| 二元搜尋 | 列 left / right / mid / a[mid] / 動作 表;注意 0-/1-indexed、 vs 、找不到時 left > right |
| BFPRT | 5 個一組求中位數 → MoM → 分堆 → 比較 與 決定遞迴哪堆 |
| Randomized Select | 期望 、最差 ;跟 BFPRT 的差異要會比 |
| Bin Packing | 背 FFD 近似比 ;會手算 WG、WE、NF/FF/BF/FFD 比較 |
| 晶片測試 | 兩兩互測、成對淘汰; |
最常考:跑一輪 BFPRT 找第 小。分組、每組中位數、MoM、、 落在哪堆——每一步都要列出來,缺一扣分。
結論
淘汰與搜尋的共通核心:每輪淘汰一固定比例、被淘汰的不再回頭。能用的條件:
- 存在低成本的比較或測試,能判定「哪部分必不含答案」
- 每輪能淘汰固定比例(),整體複雜度
Latest Updates
- 2026.04.08 Content updated
