《LeetCode 509 斐波那契数 记忆化搜索DFS解法》

一.题目

509. 斐波那契数 - 力扣(LeetCode)

二.思路讲解

2.1 思路讲解

在递归求解某些问题时,我们常常会发现递归展开图 中存在大量重复计算 。比如斐波那契数列、爬楼梯等经典问题,同样的子问题被反复求解,导致效率低下。如果能缓存 已经计算过的结果,遇到相同子问题时直接返回,就能避免重复递归 ,从而将指数级复杂度优化为多项式级。这种以空间换时间 的优化方法,就是记忆化搜索

2.2 记忆化搜索步骤

实现记忆化搜索通常遵循三个步骤:

  1. 初始化备忘录 :创建一个数组(或哈希表)来存储已经计算过的子问题的结果,并初始化为"未计算"的标记(如 -10nullptr)。

  2. 递归时先查备忘录 :在进入递归计算前,先检查备忘录中是否已有当前状态的结果。若有,则直接返回,不再重复递归。

  3. 把递归结果放入备忘录:计算出当前状态的结果后,将其存入备忘录,以便后续复用。

三.记忆化搜索、暴搜、动态规划的认识

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 == 0n == 1 时,斐波那契数即为 n 本身。此时将结果存入备忘录(memo[n] = n),并返回 n

3. 递归计算并存储

对于 n > 1,通过 dfs(n-1) + dfs(n-2) 递归计算,然后将结果存入备忘录(memo[n] = ...),最后返回该结果。

三、关键细节
  • 备忘录的作用 :将每个子问题的结果保存下来,后续遇到相同子问题时直接返回,避免了指数级的重复计算。例如,计算 fib(5) 时,fib(3) 只需计算一次,之后被多次复用。

  • 初始化值的选择:用 -1 表示"未计算",因为斐波那契数均为非负整数,-1 不会与有效结果冲突。

六、流程图