【递归、搜索与回溯】专题(八):记忆化搜索——从暴力递归到动态规划的桥梁

文章目录

    • 备忘录的魔法:拒绝重复计算
    • [一、 前言:什么是记忆化搜索?](#一、 前言:什么是记忆化搜索?)
    • [二、 斐波那契数:三阶进化的起点](#二、 斐波那契数:三阶进化的起点)
      • [2.1 题目描述](#2.1 题目描述)
      • [2.2 深度拆解:演变过程](#2.2 深度拆解:演变过程)
        • [1. 第一阶:暴力递归 (超时警告)](#1. 第一阶:暴力递归 (超时警告))
        • [2. 第二阶:记忆化搜索 (优雅降维)](#2. 第二阶:记忆化搜索 (优雅降维))
        • [3. 第三阶:动态规划 (自底向上)](#3. 第三阶:动态规划 (自底向上))
      • [2.3 C++ 代码实战(对比展示)](#2.3 C++ 代码实战(对比展示))
    • [三、 不同路径:网格中的记忆化](#三、 不同路径:网格中的记忆化)
      • [3.1 题目描述](#3.1 题目描述)
      • [3.2 深度拆解:为什么会重复?](#3.2 深度拆解:为什么会重复?)
      • [3.3 C++ 代码实战(记忆化搜索版)](#3.3 C++ 代码实战(记忆化搜索版))
    • [四、 最长递增子序列 (LIS)](#四、 最长递增子序列 (LIS))
      • [4.1 题目描述](#4.1 题目描述)
      • [4.2 深度拆解](#4.2 深度拆解)
      • [4.3 C++ 代码实战(对比)](#4.3 C++ 代码实战(对比))
    • [五、 猜数字大小 II:无法用常规 DP 的奇葩](#五、 猜数字大小 II:无法用常规 DP 的奇葩)
      • [5.1 题目描述](#5.1 题目描述)
      • [5.2 深度拆解:Min-Max 极小化极大算法](#5.2 深度拆解:Min-Max 极小化极大算法)
      • [5.3 C++ 代码实战](#5.3 C++ 代码实战)
    • [六、 矩阵中的最长递增路径](#六、 矩阵中的最长递增路径)
      • [6.1 题目描述](#6.1 题目描述)
      • [6.2 深度拆解:无需 vis 的 FloodFill](#6.2 深度拆解:无需 vis 的 FloodFill)
      • [6.3 C++ 代码实战](#6.3 C++ 代码实战)
    • [七、 终极总结:动态规划与记忆化的抉择](#七、 终极总结:动态规划与记忆化的抉择)

备忘录的魔法:拒绝重复计算

一、 前言:什么是记忆化搜索?

💬 开篇 :如果你写了一个 DFS,逻辑全对,但在力扣上提交时报了 TLE (Time Limit Exceeded 超时),那你大概率遇到了**"重叠子问题"**。

🚀 核心痛点与解药

想象你在算斐波那契数列 F ( 5 ) F(5) F(5):

  • 算 F ( 5 ) F(5) F(5) 需要算 F ( 4 ) F(4) F(4) 和 F ( 3 ) F(3) F(3)。
  • 算 F ( 4 ) F(4) F(4) 需要算 F ( 3 ) F(3) F(3) 和 F ( 2 ) F(2) F(2)。
  • 发现了吗?F ( 3 ) F(3) F(3) 被算了两遍! 当数字变大时,这种重复计算会呈指数级爆炸。

解药(记忆化搜索):准备一个"备忘录"(数组或哈希表)。

  • 进门前查字典:如果备忘录里有答案,直接拿走,不进递归!
  • 出门前写日记:算出答案后,先存进备忘录,再返回。

💡 本质 :记忆化搜索 = 动态规划的自顶向下写法。加上备忘录,暴搜的时间复杂度就瞬间降维到和动态规划一样!


二、 斐波那契数:三阶进化的起点

2.1 题目描述

题目链接509. 斐波那契数

描述

计算第 n n n 个斐波那契数。 F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n) = F(n - 1) + F(n - 2) F(n)=F(n−1)+F(n−2)。

2.2 深度拆解:演变过程

1. 第一阶:暴力递归 (超时警告)

傻乎乎地直接翻译公式。

cpp 复制代码
int fib(int n) {
    if (n == 0 || n == 1) return n;
    return fib(n - 1) + fib(n - 2);
}
2. 第二阶:记忆化搜索 (优雅降维)

创建一个 memo 数组,初始值为 -1。
两步改造法

  1. 递归开头:if (memo[n] != -1) return memo[n];
  2. 递归结尾:memo[n] = 算出的结果; return memo[n];
3. 第三阶:动态规划 (自底向上)

既然知道 F ( 2 ) F(2) F(2) 依赖 F ( 0 ) F(0) F(0) 和 F ( 1 ) F(1) F(1),那我直接从前往后推(填表)。

2.3 C++ 代码实战(对比展示)

cpp 复制代码
class Solution {
private:
    int memo[31]; // 备忘录,题目限制 n <= 30
    int dp[31];   // DP 数组

public:
    Solution() {
        memset(memo, -1, sizeof(memo)); 
    }

    // --- 方法一:记忆化搜索 (自顶向下) ---
    int fib_memo(int n) {
        // 1. 查字典:算过直接给
        if (memo[n] != -1) return memo[n];

        // 2. 递归出口
        if (n == 0 || n == 1) {
            memo[n] = n; // 别忘了存
            return n;
        }

        // 3. 递归计算,并记日记
        memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
        return memo[n];
    }

    // --- 方法二:动态规划 (自底向上) ---
    int fib_dp(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
};

三、 不同路径:网格中的记忆化

3.1 题目描述

题目链接62. 不同路径

描述

机器人从左上角走到右下角,每次只能向下或向右。求总共有多少条路径。

3.2 深度拆解:为什么会重复?

走到 (2, 2) 这个点,既可以从上方 (1, 2) 走过来,也可以从左方 (2, 1) 走过来。

如果不加备忘录,(2, 2) 走到起点的路径数会被上方和左方的递归各算一遍!

3.3 C++ 代码实战(记忆化搜索版)

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // memo[i][j] 表示从 (0,0) 走到 (i,j) 的方法数
        vector<vector<int>> memo(m, vector<int>(n, 0));
        return dfs(m - 1, n - 1, memo); // 从终点往前找
    }

    int dfs(int i, int j, vector<vector<int>>& memo) {
        // 1. 越界检查 (找不到路)
        if (i < 0 || j < 0) return 0;
        
        // 2. 递归出口:回到起点了,算 1 条有效路径
        if (i == 0 && j == 0) {
            memo[i][j] = 1;
            return 1;
        }

        // 3. 查字典
        if (memo[i][j] != 0) return memo[i][j];

        // 4. 递归计算:方法数 = 从上面来的 + 从左边来的
        memo[i][j] = dfs(i - 1, j, memo) + dfs(i, j - 1, memo);
        
        return memo[i][j];
    }
};

四、 最长递增子序列 (LIS)

4.1 题目描述

题目链接300. 最长递增子序列

描述

求数组中最长严格递增子序列的长度。

4.2 深度拆解

  • 暴搜定义dfs(pos) 表示以 pos 结尾的最长递增子序列长度。
  • 记忆化 :用 memo[pos] 存下来。

4.3 C++ 代码实战(对比)

cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        // --- 动态规划版 ---
        int n = nums.size();
        vector<int> dp(n, 1);
        int ret = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            ret = max(ret, dp[i]);
        }
        return ret;

        /*
        // --- 记忆化搜索版 ---
        vector<int> memo(n, 0);
        int max_len = 0;
        // 因为不知道以谁结尾最长,所以每个位置都要试一遍
        for(int i = 0; i < n; i++) {
            max_len = max(max_len, dfs(i, nums, memo));
        }
        return max_len;
        */
    }

    int dfs(int pos, vector<int>& nums, vector<int>& memo) {
        if (memo[pos] != 0) return memo[pos];

        int ret = 1; // 至少是自己一个
        // 往前找比自己小的人
        for (int i = 0; i < pos; i++) {
            if (nums[i] < nums[pos]) {
                ret = max(ret, dfs(i, nums, memo) + 1);
            }
        }
        memo[pos] = ret;
        return ret;
    }
};

五、 猜数字大小 II:无法用常规 DP 的奇葩

5.1 题目描述

题目链接375. 猜数字大小 II

描述

猜数字游戏,范围 1n。如果你猜错了,要支付对应的现金。求能够确保 获胜的最小现金数。

5.2 深度拆解:Min-Max 极小化极大算法

如果硬写 DP,循环的嵌套极其难写。这时候记忆化搜索直接"降维打击"。

  • 递归定义dfs(left, right) 表示在区间 [left, right] 内猜中数字的最小钱数。
  • 策略(Min) :我要在所有的猜测点 head 中,选一个花费最小的方案。
  • 最坏情况(Max) :对方非常狡猾,不管我猜什么,他总会让我付出最大的代价 (即左右两边花费更高的那一边)。
    花费 = head + max(dfs(left, head - 1), dfs(head + 1, right))

5.3 C++ 代码实战

cpp 复制代码
class Solution {
private:
    int memo[201][201] = {0};

public:
    int getMoneyAmount(int n) {
        return dfs(1, n);
    }

    int dfs(int left, int right) {
        // 区间只有一个数或没有数,不用猜,花 0 元
        if (left >= right) return 0;
        
        // 查字典
        if (memo[left][right] != 0) return memo[left][right];

        int ret = INT_MAX;
        // 枚举猜哪一个数作为切入点
        for (int head = left; head <= right; head++) {
            // 最坏情况:对方引我们去花费大的那一半
            int worst_cost = max(dfs(left, head - 1), dfs(head + 1, right));
            
            // 我们要在所有策略中选最小的代价
            ret = min(ret, head + worst_cost);
        }

        // 记日记
        memo[left][right] = ret;
        return ret;
    }
};

六、 矩阵中的最长递增路径

6.1 题目描述

题目链接329. 矩阵中的最长递增路径

描述

给定一个 m x n 矩阵,找出其中 最长递增路径 的长度。只能上下左右移动。

6.2 深度拆解:无需 vis 的 FloodFill

这是二维网格和 LIS 的结合。
亮点 :题目要求严格递增 。你从 3 走到 5,绝对不可能从 5 走回 3。递增条件天然砍断了回头路,所以不需要 vis 数组
为什么用记忆化 :从不同起点出发的路径会频繁交汇,交汇后的后半段路径一定是一样的。memo[i][j] 记录从 (i, j) 出发能走的最长长度即可。

6.3 C++ 代码实战

cpp 复制代码
class Solution {
private:
    int m, n;
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
    int memo[201][201] = {0};

public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        m = matrix.size();
        n = matrix[0].size();
        int ret = 0;

        // 任何一个格子都可能是最长路径的起点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                ret = max(ret, dfs(matrix, i, j));
            }
        }
        return ret;
    }

    int dfs(vector<vector<int>>& matrix, int i, int j) {
        // 查字典
        if (memo[i][j] != 0) return memo[i][j];

        int max_len = 1; // 哪怕无路可走,自己也算长度 1

        for (int k = 0; k < 4; k++) {
            int x = i + dx[k];
            int y = j + dy[k];

            // 不越界 && 严格递增 (天然防回头)
            if (x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) {
                max_len = max(max_len, dfs(matrix, x, y) + 1);
            }
        }

        // 记日记
        memo[i][j] = max_len;
        return max_len;
    }
};

七、 终极总结:动态规划与记忆化的抉择

💬 结业致辞:这是本系列的最后一个知识点,也是内功大成的一刻。

什么时候用 DP 填表?什么时候用记忆化搜索?

  1. 状态依赖简单、顺序明确时(如斐波那契、背包问题) :优先用 DP。因为循环填表没有递归调用的压栈开销,常数时间更优,且方便进行空间优化(滚动数组)。
  2. 状态依赖复杂、难以确定填表顺序时(如猜数字大小、矩阵递增路径) :优先用 记忆化搜索。在矩阵里,你很难通过简单的双重 for 循环保证"先填小值,再填大值"。让递归帮你自动处理拓扑排序般的依赖关系,代码写起来如丝般顺滑!

🎉 恭喜你!《递归、搜索、回溯算法》完美通关!希望这 40 道经典的算法题能成为你斩获大厂 Offer 的最强底气!咱们江湖再见! 🚀✨

相关推荐
Sincerelyplz2 小时前
【LeetForge】我用AI写了一个 LeetCode 刷题自动追踪工具,从此告别手动打卡
leetcode·cursor
豆芽包2 小时前
Git 指令大全
前端·面试
刚入坑的新人编程2 小时前
C++qt(3)-按钮类控件
开发语言·c++·qt
乐观勇敢坚强的老彭2 小时前
本周C++编程课笔记:for循环练习
java·c++·笔记
faithher2 小时前
Python 内存泄漏排查面试复盘
面试·职场和发展
飞Link2 小时前
降维打击聚类难题:高斯混合模型 (GMM) 深度解析与实战
人工智能·算法·机器学习·数据挖掘·聚类
娇娇yyyyyy2 小时前
C++ 网络编程(22) beast网络库实现websocket服务器
网络·c++·websocket
无尽的罚坐人生2 小时前
hot 100 543. 二叉树的直径
数据结构·算法·leetcode
西野.xuan2 小时前
【effective c++】条款四十三:学习处理模版化基类内的名称
java·c++·学习