Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

动态规划从哪里来

复习

  • 分治:把问题拆成更小的子问题
  • 递归:函数调用自身
  • 时间与空间的交换:用空间换时间

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

  什么是“重叠子问题”?为什么它会让普通递归变得很慢?

小结

知识点

  • 动态规划适用于有重叠子问题和最优子结构的问题
  • 分治子问题独立,动态规划子问题重叠
  • 把重复子问题只算一次是关键
  • 定义“状态”与“状态转移”是核心

参考资料

  1. Wikipedia(zh):动态规划:利用重叠子问题与最优子结构的方法
  2. Wikipedia(zh):最优子结构:整体最优可由子问题最优组合

思考题答案(仅供参考)

思考题 1

  分治的子问题相互独立、互不重叠,各算一次即可;动态规划的子问题相互重叠,同一个子问题会被反复用到。正因重叠,动态规划才需要“只算一次并缓存结果”。

思考题 2

  重叠子问题指同一个子问题会被多次遇到。比如斐波那契递归里 F(3) 被反复计算。若不缓存,每次都重新算一遍,重复量随层数急剧膨胀,导致代价指数增长。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪