动态规划

动态规划(Dynamic Programming, DP)通过将原问题拆解为重叠子问题、记录子问题最优解并自底向上或自顶向下组合,避免重复计算并构建全局最优。线性DP包括背包、最短路径、最长公共子序列/子串等;状态压缩DP利用位掩码处理子集枚举;区间DP在$l,r$区间上分治合并;树形DP在树结构上自底向上计算。下文配合典型例题与 Python 实现详解。

线性 DP

线性DP问题中,状态往往是一维或二维数组,按顺序迭代即可求解。

0/1 背包

问题:给定 (N) 件物品和容量为 (W) 的背包,每件物品重量 (w_i)、价值 (v_i),求最大总价值。

思路:定义 dp[j] 为当前容量恰为 (j) 时能取得的最大价值,倒序遍历容量避免重复使用同一物品。

def knapsack_01(weights, values, W):
    dp = [0] * (W + 1)
    for i in range(len(weights)):
        w, v = weights[i], values[i]
        for j in range(W, w - 1, -1):
            dp[j] = max(dp[j], dp[j - w] + v)
    return dp[W]

最短路径(Bellman–Ford)

问题:给定带负权但无负环的有向图,求单源最短路径。

思路:用 dist[i] 记录源点到 (i) 的最短距离,重复松弛所有边 (V-1) 轮。

def bellman_ford(n, edges, src):
    dist = [float('inf')] * n
    dist[src] = 0
    for _ in range(n - 1):
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
    return dist

最长公共子序列(LCS)

问题:给定两个字符串 AB,求它们的最长公共子序列长度。

思路:定义 dp[i][j]A[:i]B[:j] 的 LCS 长度,若 A[i-1]==B[j-1],则 dp[i][j]=dp[i-1][j-1]+1,否则取上或左最大值。

def lcs(A, B):
    n, m = len(A), len(B)
    dp = [[0]*(m+1) for _ in range(n+1)]
    for i in range(1, n+1):
        for j in range(1, m+1):
            if A[i-1] == B[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[n][m]

最长公共子串

问题:求 AB 的最长公共连续子串长度。

思路:定义 dp[i][j] 为以 A[i-1]B[j-1] 结尾的最长公共子串长度,若相等则 dp[i][j]=dp[i-1][j-1]+1,否则为 0。

def longest_common_substr(A, B):
    n, m = len(A), len(B)
    dp = [[0]*(m+1) for _ in range(n+1)]
    res = 0
    for i in range(1, n+1):
        for j in range(1, m+1):
            if A[i-1] == B[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
                res = max(res, dp[i][j])
    return res

状态压缩 DP

当子问题涉及对集合的枚举时,可用位掩码表示状态,状态空间为 (2^N)。

示例:旅行商问题(TSP)

问题:给定完全图中 (N) 个城市和对应距离矩阵 dist[i][j],求最短环路。

思路:定义 dp[mask][i] 为已访问集合 mask 且最后停在城市 i 的最短距离,转移枚举上一步城市 j

def tsp(dist):
    N = len(dist)
    INF = float('inf')
    dp = [[INF]*N for _ in range(1<<N)]
    dp[1][0] = 0  # 从城市0出发
    for mask in range(1, 1<<N):
        for i in range(N):
            if not (mask & (1<<i)): continue
            prev_mask = mask ^ (1<<i)
            for j in range(N):
                if prev_mask & (1<<j):
                    dp[mask][i] = min(dp[mask][i], dp[prev_mask][j] + dist[j][i])
    # 回到起点
    ans = min(dp[(1<<N)-1][i] + dist[i][0] for i in range(N))
    return ans

区间 DP

区间DP 常用于合并与分治场景,定义 dp[l][r] 为区间 ([l,r]) 的最优解,转移枚举分割点 k

合并石头

问题:给定石头数组 stones,每次只能合并相邻两堆,合并代价为重量之和,求最小总代价。

思路:定义 dp[l][r] 为区间 ([l,r]) 合并为一堆的最小代价,预处理 prefix[i] 为前缀和;

def mergeStones(stones):
    n = len(stones)
    pre = [0]*(n+1)
    for i in range(n): pre[i+1] = pre[i] + stones[i]
    dp = [[0]*n for _ in range(n)]
    for length in range(2, n+1):
        for l in range(n-length+1):
            r = l + length - 1
            dp[l][r] = float('inf')
            for k in range(l, r):
                cost = dp[l][k] + dp[k+1][r] + pre[r+1] - pre[l]
                dp[l][r] = min(dp[l][r], cost)
    return dp[0][n-1]

树形 DP

树形DP 在树结构上自底向上计算,每个节点的状态基于子节点结果。

二叉树直径

问题:求二叉树中任意两节点路径长度的最大值。

思路:对每个节点定义返回值为“从该节点向下的最大路径长度”,并在递归过程中更新全局直径。

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def diameterOfBinaryTree(root):
    ans = 0
    def dfs(node):
        nonlocal ans
        if not node: return 0
        left = dfs(node.left)
        right = dfs(node.right)
        ans = max(ans, left + right)
        return max(left, right) + 1
    dfs(root)
    return ans

以上内容覆盖线性DP、状态压缩DP、区间DP与树形DP,并配合典型例题与 Python 实现,助你系统梳理动态规划各大类模型与解题思路。