Joseph Chen
學完 Graph 的拓撲排序,下一個大主題是 Backtracking(回溯)。 這類題目的特徵很明顯:「列出所有可能的組合 / 排列 / 子集」。
Backtracking 的核心思想其實很像人做選擇:往前走一步,看看這條路能不能通; 不行的話,退回來(backtrack),換下一個選擇再試。 這個「退回來」的動作,在程式裡就是 path.pop()。
這篇用三道題把這個框架從頭到尾打通。
Backtracking 通用框架
先把框架背起來,後面所有題目都是這個框架的變形:
框架三個核心問題
Q: 什麼時候收集結果?
A: 每進入一層就收集(Subsets)、只在特定條件下收集(Combination Sum 加到 target)、到葉節點收集(Permutations)
Q: 枚舉什麼?
A: 從 start 開始枚舉,避免重複選取之前的元素(組合題);或枚舉所有未選過的元素(排列題)
Q: 如何剪枝?
A: 加總超過 target 就不繼續(Combination Sum);遇到重複元素就跳過(有重複的 Subsets/Permutations 變形)
Part 1 — Subsets
#78 · Medium · 列出所有子集
題目
給一個不含重複元素的整數陣列 nums, 返回所有可能的子集(冪集)。結果不能有重複的子集。
Input: nums = [1, 2, 3]
Output: [[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]
思路:每一層都是一個答案
Subsets 和一般 backtracking 最大的不同是:每進入一層遞迴,當下的 path 就是一個合法的子集。 不用等到葉節點才收集,一進函式就收集。
nums = [1, 2, 3] 的遞迴樹
每個節點進入時立刻收集 → 共 2³ = 8 個子集(空集合 + 7 個)
為什麼 Time 是 O(n × 2ⁿ)?
n 個元素的子集共有 2ⁿ 個。每個子集最多有 n 個元素需要複製(path[:]), 所以總時間是 O(n × 2ⁿ)。Space O(n) 是遞迴深度(不計結果本身)。
Part 2 — Permutations
#46 · Medium · 全排列
題目
給一個不含重複數字的陣列 nums, 返回所有可能的全排列。
Input: nums = [1, 2, 3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
Subsets vs Permutations 的差異
| Subsets(組合) | Permutations(排列) | |
|---|---|---|
| 順序重要嗎? | ❌ [1,2] 和 [2,1] 是同一個 | ✅ [1,2] 和 [2,1] 不同 |
| 如何避免重複? | 傳 start,只選後面的元素 | 用 used 集合,標記已選元素 |
| 何時收集結果? | 每一層都收集 | path 長度等於 nums 長度時 |
| 答案數量 | 2ⁿ 個 | n! 個 |
nums = [1, 2, 3],以選 1 開頭為例
以此類推,以 2、3 開頭各有 2 條路 → 共 3! = 6 個排列
💡 用 set 還是 boolean array?
上面用 set() 儲存已用的 index。 另一種常見寫法是用 used = [False] * len(nums), 效能稍好(O(1) 查詢 vs 平均 O(1) 但 worst case 略差)。 面試時兩種都可以,但 boolean array 更直觀。
Part 3 — Combination Sum
#39 · Medium · 湊到 target 的所有組合
題目
給一個不含重複元素的正整數陣列 candidates 和目標數 target。 找出所有加總等於 target 的組合,每個元素可以重複使用。
Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3], [7]]
兩個關鍵點
1. 元素可以重複使用
遞迴時傳的是 i 而不是 i+1, 允許在同一層繼續選同一個元素。
backtrack(i, path) # 可以再選 i2. 剪枝:超過 target 就停
如果目前加總已經超過 target,這條路不可能成功,直接 return。 先排序 candidates 可以讓剪枝更早發生。
if remain < 0: return # 剪枝複雜度說明
T 是 target,M 是最小的 candidate。最壞情況下遞迴樹深度是 T/M,每層最多有 n 個選擇, 所以是 O(n^(T/M))。空間是遞迴深度 O(T/M)。
candidates=[2,3,6,7], target=7 部分遞迴樹
三題統一對比
| 題目 | 何時收集 | 避免重複 | 遞迴參數 | 剪枝 |
|---|---|---|---|---|
| #78 Subsets | 每進一層 | 傳 start,往後選 | backtrack(i+1, path) | 無 |
| #46 Permutations | path 填滿時 | used set 標記 | backtrack(path, used) | used 檢查 |
| #39 Combination Sum | remain == 0 | 傳 start,往後選 | backtrack(i, path, remain) | remain < 0 return |
識別 Backtracking 題目的訊號
- →「列出所有...」「找出所有可能的...」「返回所有組合...」
- →輸入是陣列或字串,輸出是「list of list」
- →暴力做法是多層 for loop,但層數不固定
- →問題可以用「選或不選」這個決策樹來建模
常見陷阱
❌ 忘記複製 path
這是 backtracking 最常見的 bug。path 是可變物件, 直接 append 只是存了引用,之後 pop 操作會同步影響 result 裡的值。
⚠️ Combination Sum:傳 i 而非 i+1
這個細節決定「元素能不能重複選」,是這題和 Subsets 最大的不同。