Dynamic Programming(DP)是很多人覺得最難的主題,但它的核心其實只有兩件事:① 找到子問題的結構,② 把子問題的答案存起來避免重複計算。這篇用 Climbing Stairs 和 House Robber 兩道經典題,從「為什麼遞迴會超時」出發,解釋 DP 的必要性。
DP 解決的問題類型
DP 適用於「最優子結構」的問題:大問題的最優解可以由子問題的最優解推導出來,而且子問題之間有重疊。
重疊子問題
同樣的子問題被計算多次
最優子結構
大問題的最優解包含子問題的最優解
記憶化
子問題算過就存起來,避免重複
#70 Climbing Stairs
爬 n 階樓梯,每次可以爬 1 階或 2 階,有幾種方法?
先嘗試暴力遞迴的思維:到達第 n 階,只可能從第 n-1 階跨一步上來,或從第 n-2 階跨兩步上來。所以:
子問題大量重複計算,指數級爆炸。解法:把算過的答案存起來(Memoization),或由小到大填表(Tabulation)。
解法一:Top-Down Memoization(遞迴 + 快取)
解法二:Bottom-Up Tabulation(由小到大填表)
n=6 時的填表過程:
每格 = 前兩格相加。這其實就是 Fibonacci 數列!
解法三:空間優化(只需保留前兩個值)
DP 的解題框架
#198 House Robber
一排房子,每間有不同金額。你不能搶相鄰的兩間(會觸發警報),求能搶到的最大金額。
套入 DP 框架:
nums = [2, 7, 9, 3, 1] 的填表過程:
| i | nums[i] | dp[i-2]+nums[i] | dp[i-1] | dp[i] |
|---|---|---|---|---|
| 0 | 2 | — | — | 2 |
| 1 | 7 | — | 2 | 7 |
| 2 | 9 | 2+9=11 | 7 | 11 |
| 3 | 3 | 7+3=10 | 11 | 11 |
| 4 | 1 | 11+1=12 | 11 | 12 ✅ |
空間優化版(同樣的 O(1) 技巧):
兩題的共同模式
Climbing Stairs 和 House Robber 看起來不一樣,但轉移方程的結構幾乎相同:
| 題目 | dp[i] 定義 | 轉移方程 |
|---|---|---|
| Climbing Stairs | 爬到第 i 階的方法數 | dp[i] = dp[i-1] + dp[i-2] |
| House Robber | 搶到第 i 間的最大金額 | dp[i] = max(dp[i-1], dp[i-2]+nums[i]) |
兩者的核心都是:dp[i] 只依賴 dp[i-1] 和 dp[i-2],因此都可以用兩個變數把空間壓到 O(1)。這個優化技巧幾乎適用所有 1D DP。
DP 最難的不是寫程式,而是想出「狀態定義」和「轉移方程」。只要這兩件事清楚,程式幾乎自己寫出來。每次看到 DP 題,先問:「dp[i] 代表什麼,它和更小的子問題之間的關係是什麼?」
本篇重點整理