小明的魔法地图:迷宫探险智慧(记忆性递归)
🌲 故事:小明的迷宫探险与“魔法地图”
🌍 背景:神秘的数字迷宫
小明来到一座古老的迷宫。迷宫有 n
个房间,从房间 0
开始,每次可以:
走到房间
n-1
(走1步)或直接跳到房间
n-2
(走2步)
他的任务是:从房间 n
走到房间 0
,有多少种不同的路径?
这其实就是 爬楼梯问题,但我们现在把它想象成一个图上的搜索问题。
🧭 第一章:深度优先搜索(DFS)—— 小明的盲目探索
小明决定用 深度优先搜索(DFS) 来探索所有路径。
他从房间 5
出发,开始“递”:
f(5)├── f(4)│ ├── f(3)│ │ ├── f(2)│ │ │ ├── f(1) → f(0) ✅(一条路径)│ │ │ └── f(0) ✅│ │ └── f(1) → f(0) ✅│ └── f(2) → 同上 ✅└── f(3) → 又从头算一遍!😱
👉 他发现:f(3)
被算了 两次,f(2)
被算了 三次……
他累得满头大汗:“这迷宫太深了,我一直在走重复的路!”
❌ 问题:纯 DFS 会重复探索相同的子问题,效率极低,时间复杂度 O(2^n)。
🗺 第二章:魔法地图登场 —— 记忆化递归
一位智者出现,递给小明一张 “魔法地图”(memo
):
“孩子,每次你探索完一个房间,就把从那里到终点的路径总数记在地图上。下次再来,就不用再探了。”
小明恍然大悟!
🧩 魔法地图规则(记忆化递归)
class Solution {private:unordered_map<int, int> memo; // 魔法地图:记录 f(n) = 从房间 n 到 0 的路径数public:int dfs(int n) {// 🔍 第一步:进入房间 n,先看地图!if (memo.find(n) != memo.end()) {return memo[n]; // ✅ 地图有记录,直接返回!不再“递”下去!}// 🧱 基础情况:房间 1 和 2if (n == 1) return 1;if (n == 2) return 2;// 🚶•♂️ 第一次来:开始“递”——深入探索 f(n-1) 和 f(n-2)int ways = dfs(n - 1) + dfs(n - 2);// 📝 探索结束,在“归”的路上,把结果记入地图memo[n] = ways;// 🏁 返回结果return ways;}int climbStairs(int n) {return dfs(n);}};
🎬 第三章:魔法地图如何工作?—— “递” vs “归”
我们以 f(5)
为例,看看魔法地图何时“记录”,何时“优化”。
🌦 第一次探索 f(5)
:建立地图
f(5)├── f(4)│ ├── f(3)│ │ ├── f(2) → 2│ │ └── f(1) → 1│ │ → f(3) = 3,归时记入 memo[3]=3 ✅(归:记录)│ └── f(2) → 2│ → f(4)=5,归时记入 memo[4]=5 ✅(归:记录)└── f(3) → 还没记完,继续
👉 在“归”的过程中,小明把 f(3)
和 f(4)
的结果写入了魔法地图。
☀️ 第二次遇到 f(3)
:触发优化!
当 f(5)
要调用 f(3)
时:
if (memo.find(3) != memo.end()) return memo[3]; // ✅ 查表成功!
这个判断发生在 “递”的最开始!
小明甚至不用走进房间 3,直接说:“我知道,有 3 种走法!”
整个
f(3)
子树被跳过!
✅ 优化发生在这里:在“递”的过程中,查表成功,跳过递归!
🌟 关键结论:优化到底在“递”还是“归”?
阶段 | 发生了什么 | 是否优化? |
---|---|---|
第一次 f(3) 的“归” | 把 f(3)=3 写入 memo | ❌ 准备工作(无优化) |
第二次 f(3) 的“递” | 查表成功,直接返回 | ✅ 优化真正发生! |
📌 所以:
“归”是播种:把结果记下来,为未来优化做准备。
“递”是收获:在后续的“递”中,利用记忆避免重复探索。
优化的本质效果,发生在“递”的过程中。
🧠 深度优先搜索(DFS)与记忆化的关系
概念 | 对应故事中的角色 |
---|---|
DFS(深度优先搜索) | 小明一条路走到黑,深入迷宫 |
递归 | 小明不断进入新房间,靠“记忆”返回 |
函数调用栈 | 小明的“记忆栈”:记得自己是从哪个房间来的 |
记忆化(memo) | 魔法地图:记录已探索房间的结果 |
优化时机 | 在“递”时查地图,避免重复进入 |
✅ 记忆化递归 = DFS + 记忆表 它让 DFS 从“盲目探索”变成“聪明搜索”。
📊 效果对比
n | 纯 DFS(递归)调用次数 | 记忆化 DFS 调用次数 |
---|---|---|
5 | 15 次 | 9 次 |
10 | ~200 次 | 19 次 |
40 | ~1万亿次 ❌ | 79 次 ✅ |
小明从“累死也走不完”变成了“秒答如流”。
✅ 总结:记忆化递归的三大智慧
“递时查表”:每次进入函数,先查
memo
,避免重复递归。“归时记录”:计算完结果后,在返回前存入
memo
。“用空间换时间”:多用一点内存(
memo
),换来指数级的速度提升。
🚀 最终代码(带故事注释)
class Solution {private:unordered_map<int, int> memo; // 🗺 魔法地图:记录每个房间到终点的路径数public:int dfs(int n) {// 🔍 【优化发生在这里!】// 进入房间 n,先看地图:如果已经探索过,直接返回答案if (memo.find(n) != memo.end()) {return memo[n]; // ✅ 跳过整个子树,不再“递”下去}// 🧱 基础情况:房间 1 和 2if (n == 1) return 1;if (n == 2) return 2;// 🚶•♂️ 第一次来:开始“递”——深入探索子房间int ways = dfs(n - 1) + dfs(n - 2);// 📝 【归时记录】探索结束,在返回前把结果记入地图memo[n] = ways;// 🏁 返回结果return ways;}int climbStairs(int n) {return dfs(n);}};
🌈 故事的启示
递归是深度优先搜索的实现方式,而记忆化是给 DFS 装上“记忆”和“智慧”。
小明学会了:
“归时记一笔,递时省一步”
“不走重复路,才是真高手”
你也已经掌握了这门“魔法”,恭喜!🧙♂️✨
如果你还想看“回溯”、“动态规划”等更多故事版本,我也可以继续讲下去 😊