记忆化搜索
记忆化搜索
1. 基本概念
- 核心定义:记忆化搜索是一种自顶向下的算法优化技术。它在递归的基础上,增加了一个“备忘录”(通常是哈希表或数组),用来记录已经计算过的子问题的结果。
- 核心思想:“拒绝重复劳动”。
- 当你第一次遇到某个子问题时,正常计算,并把结果记在小本本上。
- 当你再次遇到相同的子问题时,直接查小本本,不再重复计算。
- 本质:它是动态规划的一种实现形式(另一种是自底向上的迭代法)。它保留了递归的代码逻辑,但拥有了动态规划的时间效率。
2. 费曼理解
用最简单的语言解释给“小白”听:
想象你在做一本超厚的数学练习册(递归问题),题目之间有依赖关系。
普通递归(傻算):
你做到第 10 题,发现需要第 5 题的答案,于是翻回去算第 5 题。
做完第 10 题,你做第 12 题,发现又需要第 5 题的答案。于是你又翻回去,重新算了一遍第 5 题!
这就叫“重复计算”,效率极低。记忆化搜索(聪明人):
你在桌子旁放了一个草稿本(Memo)。
当你第一次算出第 5 题的答案是100时,你把它记在草稿本上:“第 5 题 = 100”。
当你做第 12 题又需要第 5 题时,你不用重新算,直接看一眼草稿本,抄下100继续做。
这就叫“记忆化”。
3. 内部实现:核心结构
记忆化搜索的代码结构非常固定,通常包含以下三个部分:
| 组成部分 | 代码特征 | 作用 |
|---|---|---|
| 1. 备忘录 | memo = {} 或 @lru_cache |
用来存储 (参数) -> 结果 的映射关系。 |
| 2. 查表 | if state in memo: return memo[state] |
递归函数开头:先检查这个问题以前有没有算过。如果有,直接返回。 |
| 3. 记录 | memo[state] = result |
返回结果前:把这次算出来的结果存进去,供下次使用。 |
4. 代码实战:斐波那契数列
对比普通递归,看看记忆化搜索是如何“加速”的。
普通递归(慢):
1 | def fib(n): |
记忆化搜索(快):
1 | class Solution: |
Python 的“作弊”写法(装饰器):
你刚才发的 @lru_cache(None) 就是 Python 自动帮你写上面的 memo 逻辑。
1 | from functools import lru_cache |
5. 复杂度分析
| 指标 | 普通递归 | 记忆化搜索 |
|---|---|---|
| 时间复杂度 | 指数级 | 线性 |
| 说明 | 每个节点都分裂成两个,计算量爆炸。 | 每个子问题(状态)只计算一次,后续都是 查询。 |
| 空间复杂度 | (递归栈) | (递归栈 + 备忘录空间) |
6. 应用场景
什么时候该用记忆化搜索?
- 一看就是递归结构:题目逻辑用递归写非常自然(如树的遍历、回溯、博弈游戏)。
- 发现重复计算:在递归树中,发现很多节点的状态是重复的(比如斐波那契、爬楼梯、目标和问题)。
- 不想写迭代 DP:有时候状态转移方程很难写成
for循环(比如状态维度很多,或者依赖关系复杂),用记忆化搜索可以**“偷懒”**,直接照着逻辑写递归,加上缓存就能过。
总结:三者的关系
在你的笔记中,可以用这个公式来串联之前的知识点:
动态规划 = 递归 + 记忆化搜索
- 递归:是骨架,定义了问题的拆解方式(自顶向下)。
- 记忆化搜索:是优化手段,解决了递归的重复计算问题。
- 动态规划:是最终形态,既可以是“带缓存的递归”,也可以是“自底向上的循环”。
💡 核心感悟:
记忆化搜索是最符合人类直觉的动态规划写法。当你面对一个复杂的 DP 问题,如果不知道怎么从底向上推导,不妨先试着写一个递归,然后加上 @lru_cache,往往就能直接解决问题!
7.相关
1 | TABLE without id file.link AS "题目名称", 数据结构,date |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 笺札!










