Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

从递归到记忆化搜索

复习

  • 动态规划从哪里来:重叠子问题是关键
  • 递归:函数调用自身
  • 时间与空间的交换:缓存能省下重复计算

TL;DR

  • 记忆化搜索在递归基础上加一层缓存
  • 算过的子问题直接查表,不再重复计算
  • 它是“自顶向下”的动态规划
  • 代码接近原始递归,因此容易理解和修改

正文

  动态规划的第一个前提是“重叠子问题”。既然同一个子问题会被反复用到,那就把它只算一次、把答案记下来。这个想法的最直接实现,就是记忆化搜索(memoization)。

给递归加一个记事本

  回到斐波那契。原来的递归慢,是因为 F(3) 被算了无数遍。解决办法很简单:准备一张表,每次要算 F(n) 时先查表——

  • 表里已经有 F(n):直接返回,不再计算
  • 表里没有:老老实实递归算一遍,再把结果记进表里

  加的这点“记事本”,效果却是颠覆性的:每个子问题只会被真正计算一次,之后全是查表。代价从指数级,一下子降到线性级。

它是“自顶向下”的

  记忆化搜索的结构是自顶向下的:从原问题出发,需要哪个子问题,就递归去算哪个。

  这种方向的好处是贴合直觉。你几乎不需要改变原来的递归思路,只是在入口加一句“先查表”,出口加一句“存表”,就完成了从暴力递归到高效算法的蜕变。它把“用空间换时间”用到了极致,而且改写成本极低。

代价是什么

  当然,记忆化也有代价:

  • 仍然依靠递归,所以有调用栈的开销,深度太大时可能栈溢出
  • 需要额外的存储来缓存结果
  • 每个子问题都要走一遍函数调用,常数上比“直接填表”略大

  但它胜在写法自然、改动小。当递推关系容易写、而计算顺序不太好理清时,记忆化往往是最省心的选择。

  而如果想把递归的调用开销也省掉,就可以换成另一种方向——从最小的子问题出发,一步步往上算。这就是下一章的“自底向上”。

思考题 1

  记忆化搜索是如何消除重复计算的?

思考题 2

  记忆化搜索相比普通递归,多付出了什么?

小结

知识点

  • 记忆化搜索为递归加上结果缓存
  • 每个子问题只真正计算一次
  • 它是自顶向下的动态规划
  • 保留递归开销,但改动小、易理解

参考资料

  1. Wikipedia(zh):记忆化:缓存函数结果以避免重复计算
  2. Wikipedia(zh):动态规划:自顶向下与自底向上两种形式

思考题答案(仅供参考)

思考题 1

  它在递归入口先查缓存:若该子问题的结果已经算过,就直接返回;没算过才真正递归计算,并把结果存入缓存。这样每个子问题只被计算一次,重复计算被彻底消除。

思考题 2

  多付出的是额外的存储(用来缓存子问题结果),以及仍然保留的递归调用开销(调用栈、函数调用常数)。换来的是大幅减少的重复计算,整体效率显著提升。

协议

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

封面图

设计师 | 南国微雪