DP — 實戰題解

動態規劃與遞迴優先度 3/5 — 值得會 — 多半是必備模式的變形值得會 更新於 Nov 12, 2026

範圍 — 不是模板錨定題的經典 DP 題(LC 343、91、62、279、276、1137),每一題都是一張 problem card,歸檔在它所歸約到的 dp.md 模板底下,再加上一份「其他常見 DP 題在哪裡有完整解」的索引;本身不放模板也不談理論。 另見:dp.md — 這些 card 歸約到的模板;knapsack.md — 完整的背包家族;stock_trading.md — 所有股票狀態機的變形;dp_advanced.md — 冷門技巧與深入探討;dp_pattern.md — 更精簡的「一個模式一節」模板索引。

LeetCode 題目清單

總覽

這是 dp.md 的題目集:模板在那邊,題目在這邊。每張 card 寫出這題在模板之外多加的那一件事(每一步多一個選項、一道合法性檢查、一個不同的狀態),並給一份標準解。各節依 dp.md 的模板順序排列;題目都已在自己 card 上、或在專門的表裡解完的模板,這裡就沒有那一節。

索引

LC 題目 歸約到 難度
343 Integer Break Template 1 一維線性 DP,試遍最後一刀 Medium
91 Decode Ways Template 1a 以長度為索引的表 Medium
62 Unique Paths Template 2 二維格子 DP,計數 Medium
279 Perfect Squares knapsack.md § Template 4 完全背包,最少個數 Medium
276 Paint Fence Template 5 狀態機,兩個狀態 Medium
1137 N-th Tribonacci Number Template 6 滾動變數 Easy

在別處有完整解

每一題都只在擁有它的那張 card 或那份表裡解一次。

LC 題目 所在
53 Maximum Subarray dp.md § Template 1(錨定題);Kadane 家族在 kadane_algorithm.md
152 Maximum Product Subarray kadane_algorithm.md:同時追蹤目前的最大與最小乘積,因為負數會讓兩者對調
70 Climbing Stairs dp.md § Template 1a(錨定題)
139 Word Break dp.md § Template 1b(錨定題)
64 Minimum Path Sum dp.md § Template 2(錨定題)
416 / 494 / 1049 Partition Equal Subset Sum / Target Sum / Last Stone Weight II knapsack.md — Template 1(錨定題)與它的 LC Examples
322 / 518 / 377 Coin Change / II / Combination Sum IV knapsack.md — Template 4、2、3(錨定題)
309 Best Time to Buy and Sell Stock with Cooldown dp.md § Template 5(錨定題)
121 / 122 / 123 / 188 / 714 其他股票題 stock_trading.md
198 House Robber dp.md § Template 6(錨定題)

這裡沒有完整解 — 一句話的歸約

題目 LC 歸約到 一句話的想法
Trapping Rain Water 42 前綴/後綴陣列 water[i] = min(maxLeft[i], maxRight[i]) - h[i];雙指標可以省掉陣列
Jump Game 55 一維可達性 → 貪心 追蹤能到達的最遠索引
Jump Game II 45 一維 DP → 貪心分層 dp[i] = min(dp[j] + 1);像 BFS 那樣分層可以做到 O(n)
Minimum Cost For Tickets 983 以天為單位的 Template 1 dp[d] = min(dp[d-1] + c1, dp[d-7] + c7, dp[d-30] + c30)
01 Matrix 542 Template 2,掃兩趟 左上 → 右下,再反過來一趟
Counting Bits 338 Template 1 dp[i] = dp[i >> 1] + (i & 1)
Is Subsequence 392 LCS 的退化情況 雙指標 O(n);只有查詢很多次時才用 LCS 表
Pascal’s Triangle / II 118 / 119 Template 6,一列滾動 row[j] += row[j-1],倒著跑

一維線性 DP 範例

Integer Break — LC 343

關鍵 — dp[i] 是把 i 拆成至少兩段時的最大乘積。最後一刀 j + (i - j) 之後,剩下的 部分可以保持完整(j * (i - j)),也可以再拆(j * dp[i - j])。兩種都要:dp[2] = 1,但把 2 留著不拆值 2。

歸約到:dp.md § Template 1 — dp[i] 讀之前的每一格,每一刀有兩個選項。

python
# python
# LC 343 - Integer Break
# IDEA: dp[i] = best product of >= 2 parts summing to i; for each split, keep the rest whole or split it
# time = O(n^2), space = O(n)
def integerBreak(n):
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        for j in range(1, i):            # the first part is j
            keep_rest = j * (i - j)      # stop: i - j stays one part
            break_rest = j * dp[i - j]   # split i - j further
            dp[i] = max(dp[i], keep_rest, break_rest)
    return dp[n]
java
// java
// LC 343 - Integer Break
// IDEA: dp[i] = best product of >= 2 parts summing to i; for each split, keep the rest whole or split it
// time = O(n^2), space = O(n)
public int integerBreak(int n) {
    int[] dp = new int[n + 1];
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            int keepRest = j * (i - j);          // stop splitting: i - j stays whole
            int breakRest = j * dp[i - j];       // split i - j further
            dp[i] = Math.max(dp[i], Math.max(keepRest, breakRest));
        }
    }
    return dp[n];
}

試跑 — n = 10:

text
i      1  2  3  4  5  6  7   8   9   10
dp[i]  1  1  2  4  6  9  12  18  27  36

陷阱

  • 只用 j * dp[i - j]:剩下的 2 或 3 一定會被再拆,價值就掉了 —— n = 4 得到 3 而不是 4, n = 6 得到 6 而不是 9。
  • O(n) 的貪心(盡量拆成 3,3 + 1 改成 2 + 2)是追問,不是開場:它需要一個 DP 不需要的證明。

以長度為索引的表 範例

Decode Ways — LC 91

關鍵 — 有合法性檢查的 Climbing Stairs:dp[i](前 i 個字元的解碼方式數)在最後一位是 1–9 時加上 dp[i - 1],在最後兩位是 10–26 時加上 dp[i - 2]。0 只能當兩位數的第二位。

歸約到:dp.md § Template 1a — n + 1 的表,dp[0] = 1 代表空前綴。

python
# python
# LC 91 - Decode Ways
# IDEA: dp[i] = ways to decode s[:i]; one-digit step if s[i-1] != '0', two-digit step if 10..26
# time = O(n), space = O(n)
def numDecodings(s):
    n = len(s)
    dp = [0] * (n + 1)
    dp[0] = 1                            # the empty prefix decodes one way
    for i in range(1, n + 1):
        if s[i - 1] != "0":              # s[i-1] alone is a letter
            dp[i] += dp[i - 1]
        if i >= 2 and 10 <= int(s[i - 2:i]) <= 26:   # s[i-2:i] together is a letter
            dp[i] += dp[i - 2]
    return dp[n]
java
// java
// LC 91 - Decode Ways
// IDEA: dp[i] = ways to decode s[:i]; one-digit step if s[i-1] != '0', two-digit step if 10..26
// time = O(n), space = O(n)
public int numDecodings(String s) {
    int n = s.length();
    int[] dp = new int[n + 1];
    dp[0] = 1;                                   // the empty prefix: one way
    for (int i = 1; i <= n; i++) {
        if (s.charAt(i - 1) != '0') {            // s[i-1] decoded on its own
            dp[i] += dp[i - 1];
        }
        if (i >= 2) {
            int twoDigits = Integer.parseInt(s.substring(i - 2, i));
            if (twoDigits >= 10 && twoDigits <= 26) {   // s[i-2..i-1] decoded as a pair
                dp[i] += dp[i - 2];
            }
        }
    }
    return dp[n];
}

陷阱

  • "06" 是 0,不是 1:06 不是合法的兩位數代碼,所以檢查是 10..26,不是 <= 26。
  • 在 n + 1 的表裡,s[i - 1] 是目前的字元,不是前一個 —— 見 Template 1a 底下的 s[i-1] 陷阱。

類似 — LC 639 (有 * 萬用字元) · LC 70 · LC 1137

二維格子 DP 範例

Unique Paths — LC 62

關鍵 — 走進一格的路徑數,是走進上面那格的路徑數加上走進左邊那格的路徑數,因為最後一步 只有這兩種。第一列和第一行都是 1:到每一格只有一條直線路徑。

歸約到:dp.md § Template 2 — 把 min 換成 +,格子沒有成本。

python
# python
# LC 62 - Unique Paths
# IDEA: dp[r][c] = dp[r-1][c] + dp[r][c-1]; the first row and column are 1
# time = O(m * n), space = O(m * n)
def uniquePaths(m, n):
    dp = [[1] * n for _ in range(m)]     # row 0 and column 0 stay 1
    for row in range(1, m):
        for col in range(1, n):
            dp[row][col] = dp[row - 1][col] + dp[row][col - 1]
    return dp[m - 1][n - 1]
java
// java
// LC 62 - Unique Paths
// IDEA: dp[r][c] = dp[r-1][c] + dp[r][c-1]; the first row and column are 1
// time = O(m * n), space = O(m * n)
public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for (int row = 0; row < m; row++) {
        dp[row][0] = 1;                          // first column: only straight down
    }
    for (int col = 0; col < n; col++) {
        dp[0][col] = 1;                          // first row: only straight right
    }
    for (int row = 1; row < m; row++) {
        for (int col = 1; col < n; col++) {
            dp[row][col] = dp[row - 1][col] + dp[row][col - 1];
        }
    }
    return dp[m - 1][n - 1];
}

陷阱

  • 每一列只需要上一列,所以只開一個長度 n 的陣列、做 dp[col] += dp[col - 1],就是 O(n) 空間的追問。
  • 封閉解 C(m + n - 2, m - 1) 也對;用整數運算算它,見 combinatorics_math_patterns.md。

類似 — LC 63 (有障礙物:被擋住的格子是 0) · LC 64 · LC 980 (每格剛好走一次:回溯,不是 DP)

背包範例

Perfect Squares — LC 279

關鍵 — 平方數 1, 4, 9, … 就是無限供應的硬幣,n 是金額:湊成 n 的最少平方數個數, 就是 Coin Change。dp[0] = 0;其他每格都從 n 開始(n 個 1 永遠可行),所以沒有 -1 的情況。

歸約到:knapsack.md § Template 4 — 完全背包求最少個數,硬幣是所有 ≤ 金額的平方數。

python
# python
# LC 279 - Perfect Squares
# IDEA: unbounded knapsack, min count: squares are the coins, n is the amount
# time = O(n * sqrt(n)), space = O(n)
def numSquares(n):
    dp = [0] + [n] * n                   # dp[a] = fewest squares summing to a; n ones is the worst
    for amount in range(1, n + 1):
        root = 1
        while root * root <= amount:     # every square that fits
            dp[amount] = min(dp[amount], dp[amount - root * root] + 1)
            root += 1
    return dp[n]
java
// java
// LC 279 - Perfect Squares
// IDEA: unbounded knapsack, min count: squares are the coins, n is the amount
// time = O(n * sqrt(n)), space = O(n)
public int numSquares(int n) {
    int[] dp = new int[n + 1];
    Arrays.fill(dp, n);                          // n ones always works
    dp[0] = 0;
    for (int amount = 1; amount <= n; amount++) {
        for (int root = 1; root * root <= amount; root++) {
            dp[amount] = Math.min(dp[amount], dp[amount - root * root] + 1);
        }
    }
    return dp[n];
}

陷阱

  • 「先拿最大的平方數」的貪心會錯:12 = 9 + 1 + 1 + 1(4 個),但 4 + 4 + 4 只要 3 個。
  • 求最少個數時迴圈順序不重要;數方法數時就重要了 (knapsack.md § Loop Order)。

類似 — LC 322 · LC 518 · LC 377 — 對餘數做 BFS、一層一層走,也能解同一題。

狀態機 DP 範例

Paint Fence — LC 276

關鍵 — 最後一根柱子有兩個狀態:same(跟前一根同色)與 diff(不同色)。「不能三根連續同色」 代表 same 只能接在 diff 後面,所以 same = diff_prev,diff = (same_prev + diff_prev) * (k - 1)。

歸約到:dp.md § Template 5 — 兩個只讀前一根柱子的狀態,滾動成兩個變數。

python
# python
# LC 276 - Paint Fence
# IDEA: same = ways the last post repeats its neighbour, diff = ways it does not; roll both forward
# time = O(n), space = O(1)
def numWays(n, k):
    if n == 0:
        return 0
    if n == 1:
        return k
    same = k                             # posts 1-2 the same colour
    diff = k * (k - 1)                   # posts 1-2 different
    for _ in range(3, n + 1):
        prev_same = same
        same = diff                      # repeat a colour only right after a change
        diff = (prev_same + diff) * (k - 1)   # any of the other k - 1 colours
    return same + diff
java
// java
// LC 276 - Paint Fence
// IDEA: same = ways the last post repeats its neighbour, diff = ways it does not; roll both forward
// time = O(n), space = O(1)
public int numWays(int n, int k) {
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return k;
    }
    int same = k;                                // posts 1-2 the same colour
    int diff = k * (k - 1);                      // posts 1-2 different
    for (int i = 3; i <= n; i++) {
        int prevSame = same;
        same = diff;                             // repeat a colour only after a change
        diff = (prevSame + diff) * (k - 1);      // any of the other k - 1 colours
    }
    return same + diff;
}

陷阱

  • 用新的 same 去更新 diff:先把舊值存起來,就像每一個狀態機一樣。
  • k = 1、n ≥ 3 是 0;遞迴式自己會算到(diff 一開始就是 0)。

類似 — LC 198 · LC 309 · LC 740 (對值做拿/不拿) · LC 790 (把鋪磚當成狀態)

滾動變數範例

N-th Tribonacci Number — LC 1137

關鍵 — T(n) 剛好讀最後三個值,所以三個變數就能取代整張表。由最舊的開始往前移 (a = b、b = c、c = next),否則某個值會在被讀到之前就被覆蓋。

歸約到:dp.md § Template 6 — 最後一階,k = 3 個滾動變數。

python
# python
# LC 1137 - N-th Tribonacci Number
# IDEA: T(n) = T(n-1) + T(n-2) + T(n-3); keep only the last three values
# time = O(n), space = O(1)
def tribonacci(n):
    if n == 0:
        return 0
    if n <= 2:
        return 1
    a, b, c = 0, 1, 1                    # T(i-3), T(i-2), T(i-1)
    for _ in range(3, n + 1):
        nxt = a + b + c
        a = b                            # shift oldest first
        b = c
        c = nxt
    return c
java
// java
// LC 1137 - N-th Tribonacci Number
// IDEA: T(n) = T(n-1) + T(n-2) + T(n-3); keep only the last three values
// time = O(n), space = O(1)
public int tribonacci(int n) {
    if (n == 0) {
        return 0;
    }
    if (n <= 2) {
        return 1;
    }
    int a = 0, b = 1, c = 1;                     // T(i-3), T(i-2), T(i-1)
    for (int i = 3; i <= n; i++) {
        int next = a + b + c;
        a = b;                                   // shift oldest first
        b = c;
        c = next;
    }
    return c;
}

陷阱

  • n = 0, 1, 2 在迴圈前就回傳;起始值 0, 1, 1 必須滿足它們開頭的那個遞迴式。
  • 滾動變數的更新順序是經典 bug —— dp_advanced.md 在滾動變數的深入探討裡逐步追蹤了它。

類似 — LC 70 · LC 509 · LC 746