(xc)动态规划
一张图谱看懂DP:状态怎么设、转移怎么写、优化怎么做
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
(xc)动态规划
一张图谱看懂DP:状态怎么设、转移怎么写、优化怎么做
: 动态规划
爬楼梯时每步可跨 1 或 2 级,到第 N 级有几种走法?暴力递归会发现同一个子问题被反复算几百次——动态规划正是为消除这种浪费而生。
第一次算出第 k 级的走法就写下来,下次再问直接翻本子,不再重新爬
: Fib():递推方程
上页我们确认了 DP 的两个特征:最优子结构和重叠子问题。现在用最小、最经典的例子把它们落到纸上——Fibonacci 递推方程。
到第 n 阶的方式数 = 到 n-1 阶 + 到 n-2 阶之和,和 Fib 同构
: Fib():封底估算
递推方程写出来了,可它到底有多慢?不必真跑程序——'封底估算'就是在纸背几秒估出量级,这是判断要不要优化的第一步。
都用粗略量级,不算到分;72 法则与 2^n 同思路
: Fib():递归跟踪
上一页写了 Fib() 的递推方程,但它只是一个等式。真的去执行 Fib(5),会展开成怎样的调用树?同一层里出现了几次 F(2)、F(3)?这一页把递归过程摊开来看。
越靠近根的子问题,所在分支越多,被算的次数也越多——冗余随树深指数膨胀
: Fib():迭代
上一页看到 fib(3) 被算了两次——重复计算是最贵的浪费。迭代换个方向:从 fib(1)、fib(2) 开始往上垒,每步只看前两项。
不用记住走过的每一级台阶,只要看前两级就能算出下一级
: 最长公共子序列
Fib() 是一维递推——状态只跟一个下标挂钩。LCS 把它推到二维:dp[i][j] 表示两个前缀的 LCS 长度,依赖左上、上、左三个方向。这是 1D 到 2D DP 的标志性推广。
未修改的行串就是公共子序列,diff 工具正是基于 LCS
: LCS:递归
Fib() 递归一句话就能写完:F(n)=F(n-1)+F(n-2)。LCS 要分情况——末尾字符相等还是不等?相等怎么走、不等又怎么走?这页给出完整的递推式。
从末章往前翻,章名相同则两本都回翻;不同则只让其中一本回翻一章
: LCS:理解
你用过 git diff 吗?它怎么精准告诉你『哪些行没动』?不是逐字比,而是找出『依然在两边按顺序出现』的片段——这就是最长公共子序列(LCS)。
新版删掉的部分,就是 LCS 之外被切掉的字符
: LCS:复杂度
上页我们用递推把 LCS 拆成格子之间的依赖。这页回到老问题:这套解法到底跑多快?能不能像 Fib 那样砍掉指数爆炸?
每格只引用左、上、左上三格即可算出
: LCS:动态规划
前一页看到,朴素递归会反复计算相同后缀;把“已经算过的最长长度”保存下来,递推就从重复枚举变成逐格填表。关键是每个格子代表什么、依据哪条规则更新。
相同条款沿左上推进并加一;不同则选上方或左方较长,而且不要求条款连续。
本节要点
- ✓DP的本质:用空间换时间,消除重叠子问题的重复计算
- ✓递推方程的成立需要识别最优子结构与重叠子问题
- ✓迭代常比递归更省空间——Fib()只需两个滚动变量
- ✓复杂度先于代码:子问题数 × 单步代价 = 总成本
- ✓边界条件是DP最易出错的环节,需逐一验证
课后思考
先自己写下思路,再点开参考答案对照——题目没有标准答案。
参考答案递归版重复求解大量子问题;迭代版自底向上填值,每个子问题只算一次。根本在于有没有复用子问题的解。
参考答案DNA序列比对找最长匹配片段;论文查重找最长公共句段;git diff 本质也是求最长公共子序列。
参考答案能。LCS不要求元素唯一,DP状态只与两序列前缀长度有关,与元素出现频次解耦,重复元素不影响最优性。