跳到内容

元数据卡

  • 前置知识:ch13-dp-intro(重叠子问题、记忆化)
  • 预计时间:30 分钟
  • 核心难度:进阶
  • 完成标志:能独立写出 0-1 背包、LCS、编辑距离的 DP 解法

你的进度

上一章你学会了斐波那契——DP 的"Hello World"。现在驿站长老拿来任务清单:背包里装哪些物资价值最高、两条林间日志的最长共同记录是什么。

你的任务 掌握三个经典二维 DP 模型:0-1 背包(选择问题)、LCS(顺序匹配)、编辑距离(序列转换)。学会后你会发现 80% 的面试 DP 题是它们的变体。


0-1 背包

N 个物品,每个有重量 w[i] 和价值 v[i],背包容量 W。每个物品拿 0 或 1 个。

状态: dp[i][w] = 前 i 个物品在容量 w 下的最大价值。

python
def knapsack_01(weights, values, W):
    n = len(weights)
    dp = [[0] * (W + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(W + 1):
            if weights[i - 1] <= w:
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            else:
                dp[i][w] = dp[i - 1][w]
    return dp[n][W]

print(knapsack_01([2,3,4,5], [3,4,5,6], 8))
# 预期: 10 (选物品 3+4 价值 5+5)
java
public class Knapsack {
    public static int knapsack01(int[] w, int[] v, int W) {
        int n = w.length;
        int[][] dp = new int[n + 1][W + 1];
        for (int i = 1; i <= n; i++)
            for (int c = 0; c <= W; c++)
                if (w[i-1] <= c)
                    dp[i][c] = Math.max(dp[i-1][c], dp[i-1][c-w[i-1]] + v[i-1]);
                else
                    dp[i][c] = dp[i-1][c];
        return dp[n][W];
    }
    public static void main(String[] args) {
        System.out.println(knapsack01(new int[]{2,3,4,5}, new int[]{3,4,5,6}, 8));
    }
}
// 运行: java Knapsack  预期: 10

空间优化到一维: 反向遍历,确保每个物品只用一次。

python
def knapsack_01_opt(weights, values, W):
    dp = [0] * (W + 1)
    for i in range(len(weights)):
        for w in range(W, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[W]
print(knapsack_01_opt([2,3,4,5], [3,4,5,6], 8))  # 10

完全背包正向遍历,因为物品可以无限取。口诀:0-1 反向,完全正向。

最长公共子序列(LCS)

给定两个字符串,求最长公共子序列长度(不要求连续,顺序必须一致)。

状态: dp[i][j] = text1 前 i 字符和 text2 前 j 字符的 LCS 长度。

转移: 字符相等则 dp[i-1][j-1]+1,否则取 max(dp[i-1][j], dp[i][j-1])。

python
def lcs(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0]*(n+1) for _ in range(m+1)]
    for i in range(1, m+1):
        for j in range(1, n+1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    return dp[m][n]

print(lcs("abcde", "ace"))
# 预期: 3 ("ace")

编辑距离

每次可插入、删除、替换一个字符,求最少操作次数。

python
def min_distance(word1, word2):
    m, n = len(word1), len(word2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(m + 1): dp[i][0] = i
    for j in range(n + 1): dp[0][j] = j
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
    return dp[m][n]

print(min_distance("horse", "ros"))
# 预期: 3 (horse->rorse->rose->ros)

状态定义模板——通用框架

  1. 定义 dp 维度:一维 -> 一个序列;二维 -> 两个序列
  2. 边界条件:dp[0][] 和 dp[][0] 是什么
  3. 转移方程:dp[i][j] 依赖 dp[i-1][j], dp[i][j-1], dp[i-1][j-1] 中的哪些
  4. 遍历顺序:通常从左上到右下
  5. 答案位置:dp[m][n] 或 max(dp[*])

经典序列 DP 对比:

问题状态定义转移维度
斐波那契dp[i] = 第 i 个数dp[i] = dp[i-1]+dp[i-2]1D
0-1 背包dp[i][w] = 前 i 物容量 w 的 max 价值dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi]+vi)2D
LCSdp[i][j] = 前缀 i 和 j 的 LCS 长度字符相等则 dp[i-1][j-1]+1,否则取 max2D
编辑距离dp[i][j] = word1 前 i 到 word2 前 j 的代价相等则 dp[i-1][j-1],否则 1+min(删/插/替)2D

常见陷阱

卡点怎么办
二维转一维改错牢记:0-1 反向,完全正向
初始化漏了dp 数组多加 1,索引对齐语义
忘记代价加 1先写伪代码确认三种操作代价均为 1

通关挑战

python
def coin_change(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i:
                dp[i] = min(dp[i], dp[i - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change([1,2,5], 11))  # 3 (5+5+1)

旅人笔记

所有序列 DP 本质上都一样——到第 i 步时基于前几步的状态做一次选择。区别只在于聚合函数和边界条件。把 0-1 背包、LCS 和编辑距离吃透,剩下的 DP 变体只是在这三个模型上加加减减。


下一步:贪心算法

DP 每一步考虑所有可能。如果某个问题在每个决策点只需选"当前最优"那个——全局就是最优解呢?

看贪心算法 ->

Built with VitePress | Software Systems Atlas