🎯 什麼情境該想到我
當你「覺得『每步都選當下最好的』應該就對了」的時候。
★ 這個直覺經常是錯的,而且錯得很安靜。 所以這頁一半的篇幅在講怎麼證明它是對的。
⚙️ 怎麼用(兩個條件與兩種證明)
★ 兩個必要條件
| 條件 | 意思 |
|---|---|
| ① 貪婪選擇性質 | 局部最佳的選擇,可以導向全域最佳解(不需要回頭考慮其他選擇) |
| ② 最佳子結構 | 做完一個選擇後,剩下的仍是同型的子問題(與 動態規劃 共用這一條) |
和 DP 的關係:
貪婪 ⊂ DP 能解的問題。
DP 在每一步考慮所有選擇再取最佳;貪婪直接鎖定一個選擇不回頭。
所以貪婪更快更省,但需要更強的條件——而那個條件必須被證明。
★ 怎麼證明貪婪是對的(這才是重點)
方法一:交換論證(exchange argument)—— 最常用
假設存在一個最佳解 OPT,它和貪婪解 G 在第一個不同的地方是位置 k。
證明:可以把 OPT 在位置 k 的選擇「換成」G 的選擇,
而解的品質不會變差。
→ 反覆交換,把 OPT 變成 G,且品質始終不變
→ ★ 所以 G 也是最佳解。
方法二:歸納法 / 「貪婪保持領先」
證明貪婪在每一步都不劣於任何其他解。
★ 沒有證明就不要用貪婪。
「看起來很合理」在貪婪問題上失敗率極高——見下面的反例。
★ 經典正例與它們的證明關鍵
| 問題 | 貪婪策略 | 為什麼對 |
|---|---|---|
| 活動選擇/區間排程 | 按結束時間排序,能選就選 | 交換論證:最早結束者留給後面最多空間,換進來不會更差 |
| Huffman 編碼 | 每次合併頻率最小的兩個 | 交換論證:最小的兩個必定在最深層且互為兄弟 |
| Kruskal 最小生成樹 | 按邊權排序,不成環就加 | 割性質(cut property):任一割的最小邊必在某棵 MST 中 |
| Prim 最小生成樹 | 從樹長出去,取最小的跨邊 | 同上 |
| Dijkstra 最短路徑 | 每次取距離最小的未訪點 | ★ 非負權保證「取出即最終」 |
| 分數背包 | 按單位價值排序,裝滿為止 | 可以切割 → 交換論證成立 |
★ 經典反例(貪婪失敗的地方)
① 0-1 背包(不能切割)
容量 10
物品 A: 重 6, 值 30 → 單位價值 5.0
物品 B: 重 5, 值 20 → 單位價值 4.0
物品 C: 重 5, 值 20 → 單位價值 4.0
貪婪(按單位價值):先拿 A(重 6)→ 剩 4,B、C 都放不下 → 總值 30
★ 最佳解 :B + C(重 10) → 總值 40
「可以切割」與「不能切割」差一個字,就從貪婪可解變成必須 DP。
② 硬幣找零
幣值 {1, 3, 4},要湊 6
貪婪:4 + 1 + 1 = 3 枚
★ 最佳:3 + 3 = 2 枚
⚠️ 台幣與美元的幣值系統剛好讓貪婪成立(它們是「正則」幣值系統),
所以日常經驗會誤導你以為貪婪永遠對。 換一組幣值就錯。
③ 有負權時的 Dijkstra 最短路徑 —— 見該頁的反例。
實務上的貪婪
| 場景 | 說明 |
|---|---|
| 排程 | 最短作業優先(SJF)最小化平均等待時間 |
| 快取淘汰 | LRU / LFU 是貪婪啟發式(不保證最佳,最佳需要未來資訊) |
| 資源分配 | 裝箱問題的 first-fit / best-fit —— 是 近似演算法,不是最佳解 |
| 壓縮 | Huffman |
★ 實務上很多「貪婪」其實是啟發式(heuristic),不保證最佳。
這沒關係——但你要知道它不保證,而不是以為它最佳。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 最大的風險是「錯了不會報錯」。貪婪會給你一個看起來合理的答案,你可能永遠不知道它不是最佳。
→ 驗證方法:小規模時用暴力搜尋或 DP 算出真正的最佳解,比對。 - ★ 排序的依據是關鍵。區間排程按「結束時間」對、按「開始時間」或「長度」都錯。換一個排序鍵就是完全不同的演算法。
- 「不能切割」通常會讓貪婪失效 → 改 動態規劃。
- 貪婪對輸入順序敏感時就是警訊——正確的貪婪應該在排序後有確定的結果。
- NP-hard 問題上的貪婪是近似不是最佳,要講清楚近似比(見 近似演算法)。
- 面試時一定要說明「為什麼貪婪是對的」,直接給演算法不給論證是扣分的。
🔗 相關工具
- 動態規劃 —— 貪婪不成立時的退路;以及兩者的條件差異
- Kruskal 最小生成樹、Prim 最小生成樹、Dijkstra 最短路徑 —— 三個有嚴格證明的貪婪
- 近似演算法 —— 貪婪在 NP-hard 問題上的角色
- 分治法 —— 第三種設計範式
- 工具-演算法設計策略 —— 決策層
- 演算法導論