一.题目

二.思路讲解
2.1 思路讲解
在递归求解某些问题时,我们常常会发现递归展开图 中存在大量重复计算 。比如斐波那契数列、爬楼梯等经典问题,同样的子问题被反复求解,导致效率低下。如果能缓存 已经计算过的结果,遇到相同子问题时直接返回,就能避免重复递归 ,从而将指数级复杂度优化为多项式级。这种以空间换时间 的优化方法,就是记忆化搜索。
2.2 记忆化搜索步骤
实现记忆化搜索通常遵循三个步骤:
-
初始化备忘录 :创建一个数组(或哈希表)来存储已经计算过的子问题的结果,并初始化为"未计算"的标记(如
-1、0或nullptr)。 -
递归时先查备忘录 :在进入递归计算前,先检查备忘录中是否已有当前状态的结果。若有,则直接返回,不再重复递归。
-
把递归结果放入备忘录:计算出当前状态的结果后,将其存入备忘录,以便后续复用。
三.记忆化搜索、暴搜、动态规划的认识
3.1 记忆化搜索和动态规划
记忆化搜索 与动态规划 关系非常紧密,它们都是对暴力搜索 的优化手段,本质都是利用重叠子问题 来避免重复计算。记忆化搜索常被视为动态规划的一种自顶向下 实现形式,而动态规划通常采用自底向上的递推方式。两者在思想上是相通的,只是实现方式不同。
3.2 记忆化搜索和动态规划的区别
-
记忆化搜索 :采用递归 方式,从大问题开始,递归地分解为子问题,并缓存计算结果,属于自顶向下(Top-Down)的思路。
-
动态规划 :采用迭代 方式,从小问题开始,逐步推导出大问题的解,属于自底向上(Bottom-Up)的思路。
3.3 暴搜与二者的关系
既然记忆化搜索和动态规划都是对暴力搜索的优化,那么是否所有暴力搜索都能转换为这两种形式?不一定 。优化的前提是问题存在重叠子问题------即递归过程中有大量重复计算。如果递归树中每个子问题只出现一次(如某些树形结构问题),那么暴力搜索本身就是最优的,无需优化。此外,动态规划的状态表示往往也源自暴力搜索的递归定义。
四.代码演示
cpp
class Solution
{
public:
int memo[31];
int fib(int n)
{
//初始化
memset(memo,-1,sizeof(memo));
return dfs(n);
}
int dfs(int n)
{
//往备忘录查找一下
if(memo[n] != -1)
return memo[n];
//把递归结果放入备忘录
if(n == 0 || n == 1)
{
memo[n] = n;
return n;
}
//把递归结果放入备忘录
memo[n] = dfs(n-1) + dfs(n - 2);
return memo[n];
}
};
五.代码讲解
一、备忘录初始化
在 fib 函数中,我们首先定义一个备忘录数组 memo[31](因为题目中 n 的范围通常 ≤ 30),并使用 memset(memo, -1, sizeof(memo)) 将其所有元素初始化为 -1 ,表示对应子问题尚未计算过。然后调用递归函数 dfs(n) 开始计算。
二、递归函数 dfs
dfs(n) 负责计算第 n 个斐波那契数,并利用备忘录避免重复计算。执行流程如下:
1. 查备忘录
首先检查 memo[n] 是否不等于 -1,若是,则说明该子问题已经计算过,直接返回 memo[n],无需重复递归。这是记忆化搜索的核心优化。
2. 递归终止条件
当 n == 0 或 n == 1 时,斐波那契数即为 n 本身。此时将结果存入备忘录(memo[n] = n),并返回 n。
3. 递归计算并存储
对于 n > 1,通过 dfs(n-1) + dfs(n-2) 递归计算,然后将结果存入备忘录(memo[n] = ...),最后返回该结果。
三、关键细节
-
备忘录的作用 :将每个子问题的结果保存下来,后续遇到相同子问题时直接返回,避免了指数级的重复计算。例如,计算
fib(5)时,fib(3)只需计算一次,之后被多次复用。 -
初始化值的选择:用 -1 表示"未计算",因为斐波那契数均为非负整数,-1 不会与有效结果冲突。
六、流程图
