开始前应具备
- 理解递归和回溯
- 能分析子问题是否重复
- 熟悉二维数组
CHAPTER 09 · 状态推导
从递归和重复子问题出发,定义状态、选择与状态转移方程。
BEFORE LEARNING
动态规划解决具有重复子问题和最优子结构的问题。学习重点是状态定义与转移来源,而不是背诵题型答案。
开始前应具备
完成本章后
CORE LESSONS
LESSON
递归树中相同参数反复出现,而且大问题答案能由较小参数的答案推导。
先写出穷举所有选择的递归,再用备忘录缓存重复子问题,最后按依赖顺序改为自底向上。DP 表只是保存子问题答案的容器。
def dp(state):
if state in memo: return memo[state]
if base_case: return base
memo[state] = best(dp(next_state) for choice in choices)
return memo[state]LESSON
答案沿数组位置推进,当前选择只依赖前一个或前几个位置。
爬楼梯、最大子数组和、打家劫舍都属于线性状态,但含义不同。先明确 dp[i] 是“到达 i 的方案数”“以 i 结尾的最优值”还是“前 i 个元素的最优值”。
for i in range(1, n):
current = choose_best(previous_states, a[i])
shift(previous_states, current)LESSON
从若干物品中选择,使容量、金额或目标和满足条件;物品可能只能一次或可重复使用。
容量是状态维度,物品是决策层。0/1 背包的一维优化要倒序遍历容量,避免同一物品在本轮重复使用;完全背包则通常正序。
for item in items:
for capacity in reversed(range(item, target + 1)):
dp[capacity] = combine(dp[capacity], dp[capacity - item])LESSON
状态同时依赖两个序列位置,或一个区间的左右边界。
二维状态常表示两个前缀之间的最优关系。先处理字符相等时的转移,再处理不等时删除、跳过或替换等选择。
for i in range(1, m + 1):
for j in range(1, n + 1):
if a[i-1] == b[j-1]: match_transition()
else: choose_from_neighbors()READING
先读完本站讲义并尝试练习;遇到推导不清的地方,再回到原始教程深入阅读。
PRACTICE LADDER
题量由知识点决定,不再固定为 5 道。先完成“入门”和“模板”,再做变式与综合;挑战题可以留到复习阶段。
1 道 · 依次完成
Fibonacci Number
为什么选:从指数递归直观看到重复子问题。
4 道 · 依次完成
Climbing Stairs
为什么选:建立一维状态、初始值和空间压缩。
Maximum Subarray
为什么选:区分“以当前位置结尾”的状态定义。
House Robber
为什么选:当前房屋选择与否形成标准相邻决策。
House Robber II
为什么选:把环形约束拆为两个线性子问题。
2 道 · 依次完成
Coin Change
为什么选:完全背包中允许重复使用硬币并求最少数量。
Partition Equal Subset Sum
为什么选:把等和分割转成容量为总和一半的 0/1 背包。
4 道 · 依次完成
Longest Increasing Subsequence
为什么选:明确以 i 结尾的最长递增子序列。
Longest Common Subsequence
为什么选:两个前缀相等或不等时的二维状态转移。
Edit Distance
为什么选:综合插入、删除、替换三种选择。
Longest Palindromic Substring
为什么选:练习区间状态和按长度推进的填表顺序。
PERSONAL NOTEBOOK
笔记保存在当前浏览器