动态规划

01 / 共享的机制

一张表格逐格填充自己 —— 每一格都由 常数个更早的格子组合而来。递推关系因子模式而异, 但底层机制 —— 状态、转移、填表顺序 —— 是共通的。

02 / 一句话本质

运行后序 DFS —— 先处理子节点,再处理父节点。 每个节点返回一个值对: 偷取本节点时的最大收益,以及跳过本节点时的最大收益。
题目不偷相邻节点的最大金额[3, 4, 5, 1, 3, -, 1]
345131
二叉树上的后序 DP —— 每个节点在两棵子树都返回各自的 rob / skip 对之后再求解。
0 / 7
节点
rob
skip
答案
0 / 7

03 / 模式骨架

# 树形 DP —— 后序,每个节点返回 (rob, skip)function dp(node):if node is null: return (0, 0)(lr, ls) dp(node.left) # 先递归左子树(rr, rs) dp(node.right) # 再递归右子树rob node.val + ls + rs # 偷本节点,跳过两个子节点skip max(lr, ls) + max(rr, rs) # 每个子节点取最优return (rob, skip)lr, ls dp(root)answer max(lr, ls)

04 / 什么时候用这个模式

"必须使用后序"
父节点在两个子节点都求解完毕之前无法做决策。前序或中序会迫使你猜测子节点的值。 后序是自底向上计算的自然遍历顺序。
"返回对而非单值"
每个节点返回单个标量是不够的 —— 父节点需要知道两种结果(偷和不偷),才能做出自己的决策。返回一个值对是子树与父节点之间的最简接口。
"空节点基线为 (0, 0)"
空子节点对 rob 和 skip 的贡献都是 0。这使递推式保持统一 —— 无需在循环体中对叶子节点或只有单个子节点的节点做特殊处理。基线值能干净地向上传播。
"可推广到 rob/skip 之外"
任何节点的最优值依赖其子节点最优值的问题都遵循这一结构:后序 DFS、返回固定长度的元组、在父节点处合并。 直径(经过根的最长路径 vs. 不经过)、最大路径和、监控摄像头,都是同一骨架的变体。

05 / 常见坑

用哈希表或全局记忆化替代返回值。
常见错误:定义 dp 为从节点到值的哈希表,再用自顶向下的递归加记忆化来填充。 这样做确实可行,但隐藏了自底向上的结构。返回值方法更简洁 —— 它让数据流一目了然,且只使用 O(H) 的栈空间, 而非 O(N) 的记忆化空间。
只返回最终答案,而非完整的值对。
如果你写 dp(node) = max(rob, skip) 只返回单个数,父节点就无法区分两种情况, 会导致错误的状态转移 —— 例如误认为子节点的结果总是"跳过",从而多算收益。 务必一直返回完整的值对,直到根节点为止。
混淆合并子节点时应使用哪个值。
rob(node) = val + skip(left) + skip(right) —— 加的是子节点的 skip 值,而非 rob 值,因为偷父节点意味着禁止偷子节点。skip(node) = max(rob(L), skip(L)) + max(rob(R), skip(R)) —— 每个子节点独立选择最优。将这两条递推式互换是常见错误,会导致答案错误。

— / 什么时候用哪个

一维线性
当下标 i 处的答案只依赖常数个更早的下标时使用。
打家劫舍 · 爬楼梯 · 最大子段和
二维网格
当状态由两个下标组成 —— 网格,或两条相互作用的序列。
最短路径 · LCS · 编辑距离
背包
当每个物品都触发一次选 / 不选决策、且有容量约束时使用。
子集和 · 找零 · 划分
区间
当状态是一个区间 (i, j)、答案由内部区间组合得出时使用。
回文 · 矩阵链 · 戳气球
状态机
当每个下标可以处于若干有限状态之一、且状态之间有明确的转移规则时使用。
股票 · 冷冻期 · 双状态
树形 DP
当问题定义在上、且每个节点的答案依赖其子节点答案时使用。
打家劫舍 III · 二叉树直径 · 最大路径和
位掩码
当状态是子集(通常 N ≤ 20),且只关心选了哪些、不关心顺序时使用。
分配 · TSP · 最小覆盖团队