Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

自底向上的状态转移

复习

  • 从递归到记忆化搜索:自顶向下加缓存
  • 动态规划从哪里来:状态与转移
  • 递归与栈:递归有调用开销

TL;DR

  • 自底向上从最小的状态出发,逐步算出更大的状态
  • 它不用递归,而是按依赖顺序填表
  • 与记忆化搜索本质相同,只是方向相反
  • 关键是确定一个满足依赖的计算顺序

正文

  记忆化搜索是“自顶向下”:从大问题出发,缺哪个子问题就算哪个。还有一种方向,反过来从最小的问题开始,一步步往大推——这就是自底向上的动态规划

从小往大填表

  还是斐波那契。与其从 F(n) 往下递归,不如反过来:

F(0) = 0
F(1) = 1
F(2) = F(1) + F(0)
F(3) = F(2) + F(1)
……
一路推到 F(n)

  开一张表,从最小的 F(0)F(1) 开始,按顺序把每一项填出来。填到 F(n) 时,它依赖的两项早就填好了,直接取来相加即可。

  没有递归,没有调用栈,只有一个循环和一张表。

和记忆化是什么关系

  两者其实在算同一个东西,区别只在方向:

  • 记忆化:自顶向下,用到才算,边递归边缓存
  • 自底向上:自底向上,按顺序把每个状态都算一遍

  很多时候,把记忆化“翻个方向”,就能改写成自底向上。两者的结果一致,权衡也相近:

  • 自底向上:没有递归开销,常数更小,还可能顺着状态关系优化空间(比如斐波那契只需保存前两项,不必存整张表)
  • 记忆化:写法更贴近原始递推,只算真正用到的状态,遇到稀疏的依赖时更划算

顺序,是关键

  自底向上有一个必须小心的点:计算顺序要满足依赖。

  也就是:算出某个状态时,它依赖的所有状态必须已经算好。斐波那契里从小到大正好满足,所以顺理成章。可有些问题里,状态之间的依赖关系不那么直观,随便排个顺序就会“用到还没算的值”。

  理清状态之间的依赖、给出一个合法的计算顺序,是自底向上能否成功的前提。 这也再次说明:动态规划的难点,往往在“状态与转移的设计”和“顺序的安排”,而不在代码本身。

思考题 1

  自底向上和自顶向下(记忆化)的本质区别是什么?

思考题 2

  为什么自底向上特别在意“计算顺序”?

小结

知识点

  • 自底向上从最小状态逐步推算更大状态
  • 用循环和表代替递归
  • 与记忆化本质相同、方向相反
  • 必须保证计算顺序满足依赖

参考资料

  1. Wikipedia(zh):动态规划:自底向上的实现方式
  2. Wikipedia(zh):记忆化:与自底向上相对的实现方式

思考题答案(仅供参考)

思考题 1

  本质都是把每个子问题只算一次并保存结果。区别在方向:自顶向下从原问题出发、按需递归计算并缓存;自底向上从最小状态出发、按确定的顺序把所有状态依次算出来。方向不同,结果一致。

思考题 2

  因为算某个状态时,它依赖的状态必须先已经算好。若顺序不当,就会在计算时用到尚未求出的值,结果出错。所以必须先理清状态间的依赖,排出一个合法的计算顺序。

协议

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

封面图

设计师 | 南国微雪