动态规划从哪里来
复习
- 分治:把问题拆成更小的子问题
- 递归:函数调用自身
- 时间与空间的交换:用空间换时间
TL;DR
- 动态规划用于有“重复子问题”和“最优子结构”的问题
- 分治的子问题彼此独立,动态规划的子问题则相互重叠
- 关键是把重复的子问题只算一次
- 选好“状态”,是设计动态规划的核心
正文
贪心有时会错,回溯又太慢。介于两者之间,有一类非常强大的方法叫动态规划(dynamic programming,简称 DP)。理解它的起点,是一个“重复计算”的问题。
一个慢得离谱的递归
还记得斐波那契数列吗?F(n) = F(n-1) + F(n-2)。直接写递归:
F(n):
如果 n 是 0 或 1,返回 n
返回 F(n-1) + F(n-2)
看着简洁,却慢得吓人。因为算 F(5) 要算 F(4) 和 F(3);而算 F(4) 时又要算 F(3)——F(3) 被重复计算了很多次,越往下重复越多,代价呈指数增长。
这就引出了动态规划的第一个前提:重叠子问题——同一个子问题会被反复遇到。
和分治有什么不同
前面讲过分治,它也是把问题拆成子问题。但两者有个关键区别:
- 分治:子问题相互独立,各算各的,不会重复
- 动态规划:子问题相互重叠,同一个子问题会被多次用到
正因为重叠,动态规划才有发挥空间:既然同一个子问题要算很多遍,那就只算一次,把结果存起来。这一存,指数级的重复计算就消失了。
还需要“最优子结构”
动态规划的第二个前提是最优子结构:整个问题的最优解,可以由子问题的最优解组合出来。
比如求最短路,从 A 到 C 的最短路如果经过 B,那么 A 到 B 那段也必然是最短的——否则换一段更短的,整体就更优了。有了这个性质,我们才能放心地“用子问题的最优解拼出全局最优解”。
状态,是核心
设计动态规划,最难也最关键的一步,是定义状态:用哪些量来描述一个子问题。
状态定好了,就能写出状态转移——也就是“大状态如何由小状态推导出来”。这套“状态 + 转移”的框架,就是动态规划的灵魂。选对状态,问题就解了一半;状态选错,怎么推导都别扭。
下一章,我们先从最贴近递归的写法入手:记忆化搜索。
思考题 1
动态规划和分治的关键区别是什么?
思考题 2
什么是“重叠子问题”?为什么它会让普通递归变得很慢?
小结
知识点
- 动态规划适用于有重叠子问题和最优子结构的问题
- 分治子问题独立,动态规划子问题重叠
- 把重复子问题只算一次是关键
- 定义“状态”与“状态转移”是核心
参考资料
- Wikipedia(zh):动态规划:利用重叠子问题与最优子结构的方法
- Wikipedia(zh):最优子结构:整体最优可由子问题最优组合
思考题答案(仅供参考)
思考题 1
分治的子问题相互独立、互不重叠,各算一次即可;动态规划的子问题相互重叠,同一个子问题会被反复用到。正因重叠,动态规划才需要“只算一次并缓存结果”。
思考题 2
重叠子问题指同一个子问题会被多次遇到。比如斐波那契递归里 F(3) 被反复计算。若不缓存,每次都重新算一遍,重复量随层数急剧膨胀,导致代价指数增长。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪