Joseph Chen
EP.11 打好了 1D DP 的基礎:Climbing Stairs 和 House Robber 都是「每格只依賴前一兩格」的直線型 DP。
但現實的 DP 題更複雜。有的題目允許「無限重複選同一個元素」,有的需要在序列中找到「最長遞增的子序列」,有的要判斷「字串能不能被切成字典裡的詞」。
這三種模板,對應三道 Medium,也是進階 DP 最核心的思維跳躍。
Part 1 — Coin Change
#322 · Medium · 無限揹包(Unbounded Knapsack)
題目
給一個硬幣面額陣列 coins 和目標金額 amount, 每種硬幣可以無限使用,求最少需要幾枚硬幣能湊出 amount。無解則返回 -1。
coins = [1, 5, 11], amount = 15 → 3(5+5+5)
狀態定義
dp[i] = 湊出金額 i 所需的最少硬幣數。
答案是 dp[amount]。
轉移方程
對每個金額 i,嘗試每一枚硬幣 c:
Base case:dp[0] = 0(湊出 0 元需要 0 枚),其餘初始化為 float('inf')(代表「目前無法湊出」)。
可視化
coins = [1, 5, 11],amount = 15
初始:dp[0]=0,其餘=∞
填完後:dp[5]=1(一枚5),dp[10]=2(兩枚5),dp[15]=3
為什麼叫「無限揹包」?
經典揹包問題每件物品只能用一次(0/1 揹包)。這裡每種硬幣可以無限次使用,所以叫「完全揹包」或「無限揹包」。 關鍵差異在於:外層 loop 是 金額 i,內層 loop 是 每種硬幣, 因為當我們計算 dp[i] 時,dp[i-c] 已經允許用過同一枚硬幣了。
Part 2 — Longest Increasing Subsequence
#300 · Medium · 序列型 DP
題目
給一個整數陣列 nums, 找出最長的嚴格遞增子序列(subsequence,不要求連續)的長度。
nums = [10, 9, 2, 5, 3, 7, 101, 18] → 4([2, 3, 7, 101] 或 [2, 5, 7, 101])
狀態定義
dp[i] = 以 nums[i] 為結尾的最長遞增子序列長度。
注意:這裡 dp[i] 不是「前 i 個的答案」,而是「強制把 nums[i] 當結尾」的最長長度。答案是所有 dp[i] 的最大值。
轉移方程
往回掃所有 j < i,如果 nums[j] < nums[i],代表 nums[i] 可以接在 j 的子序列後面:
Base case:每個元素自成一個序列,所以 dp[i] = 1。
可視化
nums = [10, 9, 2, 5, 3, 7, 101, 18]
| i | nums[i] | dp[i] | 說明 |
|---|---|---|---|
| 0 | 10 | 1 | 自己一個 |
| 1 | 9 | 1 | 9 < 10,無法接在 10 後面 |
| 2 | 2 | 1 | 2 比前面都小,自己一個 |
| 3 | 5 | 2 | 5 > 2,接在 dp[2]=1 後,長度 2 |
| 4 | 3 | 2 | 3 > 2,接在 dp[2]=1 後,長度 2 |
| 5 | 7 | 3 | 7 > 2,5,3;接在 dp[3]=2 或 dp[4]=2 後,長度 3 |
| 6 | 101 | 4 | 101 > 所有;接在 dp[5]=3 後,長度 4 |
| 7 | 18 | 4 | 18 > 2,5,3,7;接在 dp[5]=3 後,長度 4 |
答案 = max(dp) = 4
🚀 進階:O(n log n) 的 Patience Sorting
面試有時會追問能不能做到 O(n log n)。答案是用「耐心排序」概念維護一個 tails 陣列,每次用 binary search 找插入位置。 但原理需要另開一篇解釋,面試前知道「有這個方法、複雜度 O(n log n)」就已足夠。
序列型 DP 的通用思路
LIS 的思路可以推廣到所有「以第 i 個元素為結尾,往前找能轉移過來的 j」的問題。 關鍵在於:dp[i] 不是「前 i 個」的最優,而是「強制以 i 結尾」的最優。 這個定義方式讓轉移方向變得清晰。
Part 3 — Word Break
#139 · Medium · 字串分割型 DP
題目
給一個字串 s 和一個字典 wordDict, 判斷 s 能否被分割成一個或多個字典裡的詞。
s = "leetcode", wordDict = ["leet","code"] → True
s = "applepenapple", wordDict = ["apple","pen"] → True
s = "catsandog", wordDict = ["cats","dog","sand","cat","an"] → False
狀態定義
dp[i] = 字串前 i 個字元(s[0:i])能否被成功分割。
注意:dp 的長度是 len(s) + 1,因為要包含「空字串」的 Base case:dp[0] = True(空字串不需要任何詞就「可以」被分割)。
轉移方程
要判斷 dp[i],就往前找所有切割點 j,如果 dp[j] 為 True 且 s[j:i] 在字典裡,則 dp[i] = True:
可視化
s = "leetcode", wordDict = {leet, code}
dp 陣列(i=0..8)
w = 字典中最長的詞長度(字串切片的比較成本)
為什麼要把 wordDict 轉成 set?
s[j:i] in wordDict 對 list 是 O(n) 查找; 轉成 set 後變成 O(1)。 這個轉換在所有「字典查找」類 DP 題都應該是反射動作。
三種 DP 模板對比
| 題目 | dp[i] 含義 | 轉移特點 | 類型 |
|---|---|---|---|
| Coin Change | 湊出金額 i 的最少硬幣 | 每種 coin 都可以無限選,外層枚舉目標 | 最佳化(無限選) |
| LIS | 以 nums[i] 結尾的最長序列 | 往前找所有合法的 j,取最大 | 序列比對 |
| Word Break | 前 i 個字元能否分割 | 往前找切割點 j,判斷子字串是否合法 | 字串分割 |
這三題學到什麼
上一篇
EP.11 — DP 入門
Climbing Stairs、House Robber
下一篇
敬請期待
Graph、Backtracking...