我爱学算法之——记忆化搜索

前言

在之前 DFS 遍历过程中,通过标记某个位置是否遍历过,从而避免每个位置进行多次 DFS 遍历;但是有些情况下,还需要获取从该位置进行 DFS 遍历的结果。

而记忆化搜索,简单来说就是记录这些 DFS 遍历的结果,在下次从某个位置进行 DFS 遍历时,如果该位置已经进行过 DFS 遍历,直接获取结果即可。

一、斐波那契数

题目解析

给定一个数 n,求第 n 个斐波那契数。

算法思路

在之前解决这道题的时,都是使用循环,计算到第 n 个斐波那契数;或者说动态规划解决。

之前不使用递归解决的原因:递归调用函数栈帧太多。

例如:要求 f(10),就要先求 f(9) 和 f(8),而求 f(9),也要去求 f(8)

通过观察不难发现,在递归遍历的过程当中,某个位置都是递归遍历了很多次的。

记忆化搜索:在遍历完某个位置时,记录一下从当前位置遍历的结果;在下次遍历的该位置时,直接使用获取结果即可。

对于这道题,就是在首次遍历完 i 位置时,记录 第i个斐波那契数,在下次遍历到 i 位置值,直接获取第 i 个斐波那契数。

这里记忆化搜索思路就有点类似于简单的动态规划问题

状态表示dp[i] 表示第 n 个斐波那契数

状态转移方程dp[i] = dp[i-1] + dp[i-2];

代码实现

cpp 复制代码
class Solution {
    int dp[33];
public:
    int f(int n) {
        if (dp[n] >= 0)
            return dp[n];
        dp[n] = f(n - 1) + f(n - 2);
        return dp[n];
    }
    int fib(int n) {
        // 初始化
        for (int i = 0; i <= n; i++)
            dp[i] = -1;
        dp[0] = 0;
        dp[1] = 1;
        return f(n);
    }
};

二、不同路径

题目解析

给定一个 m*n 的网格,只能向下、向右移动一步。

[0,0]位置出发,到达网格的右下角,求一共有多少路径。

算法思路

[0,0]位置出发,每次向下、向右移动一步,BFS 遍历展开图:

可以看出,对于同一个位置 [i,j] 是进行了多次 遍历的

记忆化搜索 :首次遍历到 [i,j]位置时,遍历完后记录结果([i,j]走到 网格右下角的路经个数 );在之后遍历到[i,j]位置时,直接获取从`[i,j] 走到 网格右下角的路径个数即可。

代码实现

cpp 复制代码
class Solution {
    int dp[110][110];
public:
    int dfs(int x, int y, int m, int n) {
        if (x > m || y > n)
            return 0;
        if (dp[x][y] > 0)
            return dp[x][y];
        dp[x][y] = dfs(x + 1, y, m, n) + dfs(x, y + 1, m, n);
        return dp[x][y];
    }
    int uniquePaths(int m, int n) {
        dp[m][n] = 1;
        return dfs(1, 1, m, n);
    }
};

三、最长递增子序列

题目解析

给定一个整数数组 nums,找出其中最长严格递增子序列的长度。

严格递增子序列:从小到大递增(不能相等),子序列(可以不连续)

算法思路

对于这道题,整体来说就是,从0 ~ n-1任意一个位置开始,递归遍历寻找最长严格递增子序列。

在遍历到 i 位置时,就要遍历寻找 从 i+1 ~ n-1开始,最长严格递增子序列。

递归遍历展开图:

虽然省略了很多内容,但还是可以看出在 递归遍历寻找最长递增子序列时,对于 i 位置是遍历了很多次的。

记忆化搜索:记录从 i 位置开始的最长递增子序列的长度;

在遍历到 i 位置时,如果 i 位置还没有遍历过,就进行一次 BFS 递归遍历;如果 i 位置已经遍历过,则直接获取结果即可。

代码实现

cpp 复制代码
class Solution {
public:
    int dfs(vector<int>& nums, vector<int>& dp, int pos) {
        int n = nums.size();
        if (pos == n)
            return 0;
        if (dp[pos] > 0)
            return dp[pos];
        int ret = 1;
        for (int i = pos + 1; i < n; i++) {
            if (nums[i] > nums[pos])
                ret = max(ret, dfs(nums, dp, i) + 1);
        }
        dp[pos] = ret;
        return dp[pos];
    }
    int lengthOfLIS(vector<int>& nums) {
        int ret = 0, n = nums.size();
        vector<int> dp(n, 0);
        for (int i = 0; i < n; i++) {
            ret = max(ret, dfs(nums, dp, i));
        }
        return ret;
    }
};

四、猜数字大小 II

题目解析

这道题,猜数字游戏:

在 1~n 中选一个数字,猜我选的数字,猜对了游戏胜利。

猜错了,告诉你我选的数字是比猜的数字 更大或者更小当猜了数字 x 并且猜错后,就需要支付 x 的现金费用),然后继续猜知道猜对为止。

给定一个数字 n ,求:无论我选择哪一个数字,都确保你能获胜的最小现金数。

算法思路

对于猜数字的整体流程,还是比较简单的:首先在[1,n]中猜数字,然后根据大小关系,再去左侧区间/右侧区间去猜数字,直到猜对为止。

这里要确保我们可以获胜,就 DFS 遍历,去求在区间[begin,end]内 任选一个数字,都能获取胜利的最小现金数。

对于区间[begin,end],猜数字 i ,要确保能够获得胜利,就要分别求 区间[begin,i-1][i+1,end]内数字能获得胜利的最小现金数,然后取最大值(对于任选数字,都要保证能获取胜利

对于区间 [begin, end],计算该区间内必胜的最小现金数 dfs(begin, end)

  • 边界条件 :若 begin >= end(区间无数字 / 只有 1 个数字),无需花钱,直接返回 0
  • 枚举猜测点:遍历区间内每个数字 i分析猜 i 的代价:
    • 若答案比 i 小:DFS 计算左区间 [begin, i-1] 的必胜最小代价 dfs(begin, i-1)
    • 若答案比 i 大:DFS 计算右区间 [i+1, end] 的必胜最小代价 dfs(i+1, end)
    • 「最坏情况」:猜 i 时,需准备足够的钱,取左右区间中代价更高的那个(否则钱不够会输),即 max(left, right)
    • 「猜 i 的总成本」:最坏情况代价 + 猜 i 本身的成本 i,即 max(left, right) + i
  • 选最优策略 :枚举所有猜测点 i 后,取所有 i 对应的总成本的最小值(区间[begin,end]的最小必胜现金数)。

记忆化搜索 :在DFS遍历过程中,区间 [i,j]就会被 DFS 遍历多次,这里就采用二维数组来记录 区间[i,j]最小必胜现金数。

代码实现

cpp 复制代码
class Solution {
    int dp[210][210];
public:
    int dfs(int begin, int end) // [begin,end] 能猜中数字,最小现金数
    {
        if (begin >= end)
            return 0;
        if (dp[begin][end] > 0)
            return dp[begin][end];
        int ret = 0x3f3f3f;
        for (int i = begin; i <= end; i++) {
            int left = dfs(begin, i - 1);
            int right = dfs(i + 1, end);
            ret = min(ret, max(left, right) + i);
        }
        dp[begin][end] = ret;
        return dp[begin][end];
    }
    int getMoneyAmount(int n) { return dfs(1, n); }
};

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

题目解析

给定一个 m*n 的矩阵,可以向上、下、左、右四个方向移动(不能沿对角线移动,不能移动到边界外、也不允许环绕)

要找出矩阵当中,最长的递增路径。

算法思路

最长递增路径,可以以任意位置开始,所以就要从二维矩阵的每一个位置都进行 BFS遍历,寻找最长递增路径。

在 BFS 递归遍历寻找最长路径时,对于[i,j],也会遍历很多次的。

记忆化搜索 :记录以 [i, j]位置为起点的最长递增子路径的长度(首次遍历时记录,后续遍历直接获取结果)

代码实现

cpp 复制代码
class Solution {
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1};

public:
    int dfs(vector<vector<int>>& matrix, vector<vector<int>>& dp, int x,
            int y) {
        if (dp[x][y] > 0)
            return dp[x][y];
        int m = matrix.size();
        int n = matrix[0].size();
        int ret = 1;
        for (int i = 0; i < 4; i++) {
            int posx = x + dx[i];
            int posy = y + dy[i];
            if (posx >= 0 && posx < m && posy >= 0 && posy < n &&
                matrix[posx][posy] > matrix[x][y])
                ret = max(ret, dfs(matrix, dp, posx, posy) + 1);
        }
        dp[x][y] = ret;
        return dp[x][y];
    }
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector<vector<int>> dp(m, vector<int>(n, 0));
        int ret = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                ret = max(ret, dfs(matrix, dp, i, j));
            }
        }
        return ret;
    }
};

本篇文章到这里就结束了,感谢支持

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

相关推荐
m0_730115112 小时前
C++与Python混合编程实战
开发语言·c++·算法
郝学胜-神的一滴4 小时前
Leetcode 969 煎饼排序✨:翻转间的数组排序艺术
数据结构·c++·算法·leetcode·面试
I_LPL11 小时前
hot100贪心专题
数据结构·算法·leetcode·贪心
颜酱12 小时前
DFS 岛屿系列题全解析
javascript·后端·算法
WolfGang00732112 小时前
代码随想录算法训练营 Day16 | 二叉树 part06
算法
2401_8318249613 小时前
代码性能剖析工具
开发语言·c++·算法
Sunshine for you14 小时前
C++中的职责链模式实战
开发语言·c++·算法
qq_4160187214 小时前
C++中的状态模式
开发语言·c++·算法
2401_8845632414 小时前
模板代码生成工具
开发语言·c++·算法