基礎 DP 的挑戰是「找到轉移方程式」;進階 DP 的挑戰是「想到要定義什麼樣的狀態」。
問題升級的關鍵
基礎 DP 的問題,狀態通常是「處理到第 i 個、最優值是多少」——一維的。
進階 DP 的問題開始出現二維狀態(兩個字串的位置)、區間狀態(一段字串)、指數狀態(子集的集合)。這些狀態更難想到,但轉移往往並不複雜。
Edit Distance:量化兩個字串的差距
把 "horse" 改成 "ros" 最少幾步(插入/刪除/替換各算 1 步)?
這是 Levenshtein distance——Git 的 diff 輸出、Google 拼字修正、DNA 序列比對,核心都是這個問題。
狀態:dp[i][j] = 把 s1 的前 i 個字元改成 s2 的前 j 個字元的最少步數
轉移:
s1[i] == s2[j]:dp[i][j] = dp[i-1][j-1](不需要操作)
s1[i] != s2[j]:dp[i][j] = min(
dp[i-1][j] + 1, // 刪除 s1[i]
dp[i][j-1] + 1, // 插入 s2[j]
dp[i-1][j-1] + 1 // 替換 s1[i] 為 s2[j]
)
"" r o s
"" 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3
結果:dp[5][3] = 3 步
區間 DP:矩陣鏈乘法
A(10×30) × B(30×5) × C(5×60) 怎麼加括號算最快?
(A×B)×C:10×30×5 + 10×5×60 = 4,500 次乘法
A×(B×C):30×5×60 + 10×30×60 = 27,000 次乘法
差了 6 倍。矩陣鏈乘法找最優加括號方案:
dp[i][j] = 矩陣 i 到 j 連乘的最小乘法次數
對所有分割點 k(i ≤ k < j):
dp[i][j] = min(dp[i][k] + dp[k+1][j] + rows[i] × cols[k] × cols[j])
建表順序:先算長度小的區間,再用它們組合長區間
這類「區間 DP」的標誌是「先處理小區間,再組合大區間」。石子合併、最優括號化、回文分割,都是這個模式。
Bitmask DP:子集的狀態
旅行業務員問題(TSP):n 個城市各走一遍,求最短路徑。暴力是 O(n!),n=15 就是 10^12——無法接受。
Bitmask DP 用一個整數表示「哪些城市已訪問」:
dp[mask][v] = 已訪問城市集合為 mask、當前在城市 v 的最短距離
轉移:從 v 走到未訪問的城市 u
dp[mask | (1 << u)][u] = min(dp[mask | (1 << u)][u], dp[mask][v] + dist[v][u])
狀態數:2^n × n;轉移:O(n)
總複雜度:O(2^n × n²)
n=20:2^20 × 400 ≈ 4 億,邊界可接受
Egg Drop:反向定義狀態
k 顆蛋、n 層樓,找臨界樓層最少需要幾次?
直接定義 dp[k][n] = k 顆蛋 n 層樓需要幾次,轉移複雜(每層都是分割點),O(kn²) 太慢。
反向定義:
dp[t][k] = t 次嘗試 + k 顆蛋,能確定的最多樓層數
dp[t][k] = dp[t-1][k-1] + dp[t-1][k] + 1
蛋碎了(往下找) 蛋沒碎(往上找) 當前這層
結果:找最小的 t 使得 dp[t][k] ≥ n
進階 DP 很少卡在轉移方程式——通常卡在「原來狀態要這樣定義」這一步。