範圍 — 每一步選當下最好的,以及證明這樣做安全的交換論證——區間排程、跳躍遊戲、任務分配——外加怎麼看出貪婪會失敗、必須改用 DP 的時機。
另見:greedy_examples.md — 撐起這些模板的十四道題目詳解;intervals.md — 區間類的貪婪家族;heap.md — 需要反覆取出當前最佳值的貪婪;dp.md — 交換論證垮掉時的退路;sort.md — 幾乎每個貪婪都從排序開始。
貪婪演算法在每一步選擇局部最佳解,期望最後能得到全域最佳解。它在每個決策點挑當下可得的最好選項,而且不回頭檢討先前的決定。
- 時間複雜度:通常是 O(n),需要排序時是 O(nlogn)
- 空間複雜度:視題目而定,O(1) 到 O(n)
- 核心想法:每一步都做局部最佳的選擇
- 什麼時候用:具備貪婪選擇性質與最佳子結構的問題
- 限制:不保證得到全域最佳解
- 貪婪選擇性質:局部最佳能導向全域最佳
- 最佳子結構:最佳解包含子問題的最佳解
- 不回溯:一旦做了選擇,就不再重新考慮
- 必須證明:得證明貪婪確實會得到最佳解
- 貪婪 vs DP:貪婪選擇成立時,貪婪就是被最佳化過的 DP
- 貪婪 vs 暴力:快很多,但可能錯過最佳解
- 演進路線:暴力 → DP → 貪婪(在適用的情況下)
| 模板類型 |
適用情境 |
排序依據 |
什麼時候用 |
| 區間 |
挑出互不重疊的區間 |
結束時間 |
會議室、活動安排 |
| 優先佇列 |
動態挑選 |
值/頻率 |
任務排程 |
| 雙指標 |
配對/匹配 |
視情況 |
陣列操作 |
| 累積 |
累加和/累乘積 |
不需排序 |
股票、加油站 |
| 跳躍/可達 |
追蹤位置 |
不需排序 |
跳躍遊戲 |
def greedy_solution(items):
items.sort(key=lambda x: x[criterion])
result = initial_value
current_state = initial_state
for item in items:
if can_select(item, current_state):
result = update_result(result, item)
current_state = update_state(current_state, item)
return result
★★★★★Priority 5 of 5 — Must know — expect it in almost every loop
def interval_scheduling(intervals):
"""Select maximum non-overlapping intervals"""
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
count = 1
end = intervals[0][1]
for i in range(1, len(intervals)):
if intervals[i][0] >= end:
count += 1
end = intervals[i][1]
return count
★★★★☆Priority 4 of 5 — High value — a gap here costs you rounds
import heapq
import collections
def activity_selection_heap(tasks):
"""Select activities using priority queue"""
freq = collections.Counter(tasks)
heap = [(-count, task) for task, count in freq.items()]
heapq.heapify(heap)
result = []
while heap:
count1, task1 = heapq.heappop(heap)
result.append(task1)
if heap:
count2, task2 = heapq.heappop(heap)
result.append(task2)
if count1 < -1:
heapq.heappush(heap, (count1 + 1, task1))
if count2 < -1:
heapq.heappush(heap, (count2 + 1, task2))
return result
★★★★☆Priority 4 of 5 — High value — a gap here costs you rounds
def greedy_accumulation(prices):
"""Accumulate positive differences (stock trading)"""
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
★★★★★Priority 5 of 5 — Must know — expect it in almost every loop
def jump_game(nums):
"""Check if can reach end"""
max_reach = 0
for i in range(len(nums)):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
if max_reach >= len(nums) - 1:
return True
return True
def jump_game_min_jumps(nums):
"""Minimum jumps to reach end"""
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
★★★☆☆Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
def reorganize_string(s):
"""Reorganize string so no adjacent chars are same"""
from collections import Counter
import heapq
count = Counter(s)
max_count = max(count.values())
if max_count > (len(s) + 1) // 2:
return ""
heap = [(-cnt, char) for char, cnt in count.items()]
heapq.heapify(heap)
result = []
prev_count, prev_char = 0, ''
while heap:
count, char = heapq.heappop(heap)
result.append(char)
if prev_count < 0:
heapq.heappush(heap, (prev_count, prev_char))
prev_count = count + 1
prev_char = char
return ''.join(result)
def fractional_knapsack(items, capacity):
"""Greedy knapsack allowing fractions"""
items.sort(key=lambda x: x[0]/x[1], reverse=True)
total_value = 0
remaining = capacity
for value, weight in items:
if weight <= remaining:
total_value += value
remaining -= weight
else:
total_value += value * (remaining / weight)
break
return total_value
| 題目 |
LC # |
關鍵技巧 |
Difficulty |
| Non-overlapping Intervals |
435 |
依結束時間排序 |
Medium |
| Minimum Arrows to Burst Balloons |
452 |
依結束時間排序 |
Medium |
| Maximum Length of Pair Chain |
646 |
依結束時間排序 |
Medium |
| Merge Intervals |
56 |
依起始時間排序 |
Medium |
| Meeting Rooms II |
253 |
排序+堆積 |
Medium |
| Interval List Intersections |
986 |
雙指標 |
Medium |
| 題目 |
LC # |
關鍵技巧 |
Difficulty |
| Task Scheduler |
621 |
頻率統計 |
Medium |
| Maximum Events Attended |
1353 |
排序+堆積 |
Medium |
| Course Schedule III |
630 |
排序+堆積 |
Hard |
| IPO |
502 |
兩個堆積 |
Hard |
| 題目 |
LC # |
關鍵技巧 |
Difficulty |
| Buy Sell Stock II |
122 |
累加每一段漲幅 |
Easy |
| Gas Station |
134 |
環狀陣列 |
Medium |
| Best Time with Fee |
714 |
狀態追蹤 |
Medium |
| Container With Most Water |
11 |
雙指標 |
Medium |
| 題目 |
LC # |
關鍵技巧 |
Difficulty |
| Jump Game |
55 |
追蹤最遠可達位置 |
Medium |
| Jump Game II |
45 |
最少跳躍次數 |
Medium |
| Jump Game III |
1306 |
BFS/DFS |
Medium |
| Reach a Number |
754 |
數學+貪婪 |
Medium |
| 題目 |
LC # |
關鍵技巧 |
Difficulty |
| Reorganize String |
767 |
最大堆積 |
Medium |
| String Without AAA or BBB |
984 |
貪婪+計數追蹤 |
Medium |
| Rearrange K Distance Apart |
358 |
堆積+佇列 |
Hard |
| Task Scheduler |
621 |
頻率 |
Medium |
| Longest Happy String |
1405 |
堆積貪婪 |
Medium |
| 題目 |
LC # |
關鍵技巧 |
Difficulty |
| Candy |
135 |
兩趟掃描 |
Hard |
| Assign Cookies |
455 |
雙指標 |
Easy |
| Maximum Units on Truck |
1710 |
依價值排序 |
Easy |
| Boats to Save People |
881 |
雙指標 |
Medium |
| Minimum Cost to Connect Sticks |
1167 |
最小堆積 |
Medium |
| Max Non-Overlapping Subarrays Sum=Target |
1546 |
前綴和+貪婪重置 |
Medium |
| Car Fleet |
853 |
由前往後排序,數抵達時間的最大值出現幾次 |
Medium |
| Count Robot Groups |
4045 |
由右往左掃,比速度(沒有終點) |
Medium |
上面這兩題通常被當成堆疊題教,但它們不是。 LC 853 的教科書解法只把東西
推進堆疊、從來不彈出,所以 stack[-1] 只是一個滾動最大值、len(stack) 只是
一個計數器 — 兩個變數就能在 O(1) 空間做完同一件事。LC 4045 也一樣。這個家族裡
堆疊真正值得存在的地方是 LC 1776(Car Fleet II):那裡已經算出來的答案有可能被
推翻,於是你真的需要彈出。完整對照見
monotonic_stack.md。
面試官很愛出那種看起來像貪婪的題目。能講出反例並馬上換路走,價值不輸把貪婪寫出來。
| 題目 |
LC # |
誘人的貪婪想法 |
為什麼會爛掉 |
真正可行的做法 |
| Split Array Largest Sum |
410 |
「累積和一超過 total/k 就切一刀」 |
那個門檻事先根本不知道;局部塞滿的一段會逼出一個超大的尾段 |
對答案二分搜尋+一個貪婪的可行性檢查(最大和 <= X 時,能不能切成 <= k 段?)。貪婪在這裡是 O(N) 的驗證器,不是最佳化器。O(N log(sum)) |
| Wildcard Matching |
44 |
「從左到右逐字比對,* 需要多少就吃多少」 |
* 吃太快,後面的字面字元就沒東西可配 |
要嘛用 DP O(S*P),要嘛用帶回溯錨點的雙指標貪婪(記住上一個 * 的位置,不匹配就倒回去) |
| Best Time to Buy/Sell with Fee |
714 |
「把每個正的差值加起來」(LC 122 那一套) |
手續費是按每筆交易收的,所以小漲幅可能反而是負的 |
DP 狀態機 hold / cash — 見上面的股票交易表 |
| 0/1 Knapsack |
— |
「依價值/重量比排序」 |
物品不能切開(反例見下面的分數 vs 0/1 背包表) |
DP O(nW) |
辨識的經驗法則:
- 目標如果是*「最小化最大值」/「最大化最小值」* → 貪婪通常會變成對答案二分搜尋裡的那個單調判斷式,而不是一個獨立的演算法(LC 410 是最典型的例子)。
- 如果一個選擇之後可以划算地反悔(手續費、上限、能撤回的截止期限)→ 解法往往是基於堆積的「反悔」貪婪,而不是 DP。見
heap_advanced.md 裡的 greedy with regret 模板 — LC 871 Minimum Number of Refueling Stops、LC 630 Course Schedule III、LC 1642 Furthest Building You Can Reach。
| 題目 |
LC # |
一句話講完的貪婪 |
Difficulty |
| Valid Palindrome II |
680 |
雙指標;第一次不匹配時,試著跳過任一邊再檢查剩下的部分 |
Easy |
| Minimum Domino Rotations For Equal Row |
1007 |
目標值只可能是 tops[0] 或 bottoms[0]——只要驗這兩個候選 |
Medium |
| Increasing Triplet Subsequence |
334 |
記住目前看過的最小值與次小值;出現第三個把兩者都比下去就是 true |
Medium |
| Largest Number |
179 |
用自訂比較器 a+b vs b+a(字串串接)排序 |
Medium |
| Hand of Straights / Divide Array in Sets of K |
846 / 1296 |
每一組永遠從剩下最小的那張牌開始 |
Medium |
| Minimum Increment to Make Array Unique |
945 |
排序後,把每個元素推到 max(x, prev+1) |
Medium |
| Can Place Flowers |
605 |
由左往右掃,看到第一個合法的位置就種下去 |
Easy |
Greedy Algorithm Selection Flowchart:
1. Can the problem be solved greedily?
├── Does local optimal lead to global optimal? → YES → Use Greedy
├── Can you prove greedy correctness? → YES → Use Greedy
└── NO to both → Use DP or other approach
2. What type of greedy pattern?
├── Selection from sorted items → Interval/Activity Selection
├── Maximize/minimize at each step → Accumulation Pattern
├── Dynamic selection → Priority Queue/Heap
├── Position/reach tracking → Jump Game Pattern
└── Pairing/matching → Two Pointers
3. How to make greedy choice?
├── Sort by what criterion?
│ ├── End time → Interval scheduling
│ ├── Start time → Merge intervals
│ ├── Value/weight ratio → Knapsack
│ └── Custom criterion → Problem specific
└── No sorting needed → Direct iteration
4. Common greedy strategies:
├── Always take the best available
├── Never make a choice that blocks future options
├── Minimize waste/maximize efficiency
└── Balance resources evenly
| 判準 |
用貪婪 |
用 DP |
例子 |
| 具備貪婪選擇性質 |
✅ |
❌ |
活動選擇 |
| 需要所有子問題的解 |
❌ |
✅ |
0/1 Knapsack |
| 能證明最佳性 |
✅ |
- |
Huffman coding |
| 子問題會重疊 |
❌ |
✅ |
Fibonacci |
| 選擇規則很單純 |
✅ |
❌ |
分數背包 |
要驗證一個貪婪選擇,就證明把貪婪挑的那個換成任何其他選擇,結果都不會變好。
1. Assume optimal solution OPT differs from greedy solution G at some step.
2. Show you can swap OPT's choice at that step with G's choice without making things worse.
3. Repeat until OPT == G → greedy is optimal.
常見的交換論證題目:LC 435(Non-overlapping Intervals)、LC 452(Burst Balloons)、工作排程。
Kruskal(把邊排序,配併查集):
def kruskal(n, edges):
edges.sort(key=lambda x: x[2])
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
a, b = find(a), find(b)
if a == b: return False
parent[a] = b
return True
mst_cost = 0
for u, v, w in edges:
if union(u, v):
mst_cost += w
return mst_cost
Prim(優先佇列,適合稠密圖):
import heapq
from collections import defaultdict
def prim(n, edges):
graph = defaultdict(list)
for u, v, w in edges:
graph[u].append((w, v))
graph[v].append((w, u))
visited = set()
heap = [(0, 0)]
total = 0
while heap and len(visited) < n:
cost, node = heapq.heappop(heap)
if node in visited: continue
visited.add(node)
total += cost
for w, nei in graph[node]:
if nei not in visited:
heapq.heappush(heap, (w, nei))
return total
| 演算法 |
Time |
適合 |
| Kruskal |
O(E log E) |
稀疏圖 |
| Prim (heap) |
O(E log V) |
稠密圖 |
區間帶有權重/利潤時,光靠貪婪會失敗——要用 DP+二分搜尋。
import bisect
def jobScheduling(startTime, endTime, profit):
jobs = sorted(zip(startTime, endTime, profit), key=lambda x: x[1])
dp = [(0, 0)]
for s, e, p in jobs:
i = bisect.bisect_right(dp, (s, float('inf'))) - 1
new_profit = dp[i][1] + p
if new_profit > dp[-1][1]:
dp.append((e, new_profit))
return dp[-1][1]
| 性質 |
分數背包 |
0/1 背包 |
| 物品能否切開 |
可以 |
不行 |
| 演算法 |
貪婪(依價值/重量排序) |
DP |
| Time |
O(n log n) |
O(nW) |
| 貪婪可行嗎? |
可行 |
不可行 |
為什麼 0/1 背包不能用貪婪:反例:物品 [(value=6, w=4), (value=5, w=3), (value=5, w=3)],容量 = 6。貪婪挑比值最高的(item1,ratio=1.5)→ 只拿到 6。DP 挑 item2+item3 → 拿到 10。
| 訊號 |
對應模式 |
| 「連接起來的最小成本」 |
MST(Kruskal/Prim) |
| 「最大化互不重疊的區間數」 |
依結束時間排序 |
| 「帶冷卻時間的任務排程」 |
數學公式或最大堆積 |
| 「物品可以切成小份」 |
依價值/重量比排序 |
| 「證明這個貪婪是對的」 |
交換論證 |
| 「這裡貪婪會給錯答案」 |
改用 DP |
| 模式 |
時間複雜度 |
空間複雜度 |
瓶頸 |
| 區間排程 |
O(nlogn) |
O(1) |
排序 |
| 用堆積挑選 |
O(nlogn) |
O(n) |
堆積操作 |
| 雙指標 |
O(n) 或 O(nlogn) |
O(1) |
需要時的排序 |
| 直接累積 |
O(n) |
O(1) |
單趟掃描 |
| 跳躍遊戲 |
O(n) |
O(1) |
單趟掃描 |
intervals.sort(key=lambda x: x[1])
intervals.sort(key=lambda x: x[0])
items.sort(key=lambda x: x.value/x.weight, reverse=True)
tasks.sort(key=lambda x: (x.deadline, -x.profit))
def exchange_argument_proof(arr):
pass
def stays_ahead_proof(greedy, other):
pass
- 判斷有沒有貪婪的可能:找找看有沒有最佳子結構
- 定義貪婪選擇:每一步要挑什麼
- 證明正確性:交換論證或貪婪永遠領先
- 有效率地實作:通常需要先排序
- 處理邊界情況:空輸入、只有一個元素
- 用例子驗證:把貪婪選擇實際跑一遍
🚫 常見錯誤:
- 沒證明就假設貪婪成立
- 排序依據挑錯
- 沒把所有邊界情況考慮進去
- 忘記處理平手的情況
- 漏掉全域限制的檢查
✅ 最佳實務:
- 一定先確認貪婪性質成立
- 從小例子開始
- 主動想反例
- 需要動態挑選時就用堆積
- 拿邊界情況測一遍
- 辨識模式:留意題目裡關於排序或挑選的暗示
- 從例子下手:先把小案例走一遍
- 講清楚假設:說明貪婪在什麼前提下適用
- 被問到就證明:用交換論證或貪婪永遠領先
- 把程式寫乾淨:貪婪的程式碼通常很短
- 再最佳化:想想能不能用堆積把複雜度壓下來
- 活動選擇:挑出最多互不重疊的活動
- Huffman 編碼:建出最佳的前綴碼
- Kruskal MST:挑權重最小的邊
- Dijkstra(戴克斯特拉):挑距離最小的頂點
- 分數背包:優先拿比值最高的
- 動態規劃:貪婪行不通時
- 二分搜尋:處理最佳化問題
- 堆積/優先佇列:需要動態挑選時
- 排序:貪婪的常見前置步驟
- 圖論演算法:很多都建立在貪婪上(MST、最短路徑)
十四道題目放在 greedy_examples.md,是按貪婪選擇的形狀分組,
而不是按主題——因為你在面試現場真正要認出來的正是形狀:
| 分組 |
你做的那個選擇 |
題目 |
| Reach & jump |
一路延伸最遠可達點,非跳不可時才跳 |
LC 55, 45, 1326 |
| Accumulate & reset |
有賺就拿;累計值一變負就歸零重來 |
LC 122, 134, 1546, 921 |
| Frequency & heap interleaving |
永遠放當下合法且出現次數最多的那個 |
LC 767, 984, 621 |
| Sort, then take |
依關鍵比值排序,然後照順序拿 |
LC 1710, 3994 |
| Build while scanning |
一個字元或一條邊界確定不會再變,就馬上定案 |
LC 763, 402 |