算法:DFS之记忆化搜索

news2024/9/23 21:30:30

目录

记忆化搜索

题目一:不同路径

题目二:最长递增子序列

题目三:猜数字大小II

题目四:矩阵中的最长递增路径


记忆化搜索

说到记忆化搜索,首先就需要引入斐波那契数这道题,非常经典,可以很好地说明什么是记忆化搜索

斐波那契数列

首先是列举递归的做法:

class Solution 
{
public:
    int fib(int n) 
    {
        return dfs(n);
    }

    int dfs(int n)
    {
        if(n == 0 || n == 1)
            return n;
        return dfs(n - 1) + dfs(n - 2);
    }
};

这种做法非常简便,但是时间复杂度也非常高,近似于O(2^N)

而之所以慢,是因为有很多数都重复递归计算了,例如dfs(5)时,会计算dfs(4)和dfs(3),其中计算dfs(4)时,又会计算dfs(3)和dfs(2)
dfs(5)和dfs(4)都重复计算dfs(3)了,再往下看,重复计算的数更多,所以就会导致时间复杂度非常高

而如果想优化也非常简单,我们每计算出一个值,就将其放入“备忘录”中,如果后续又需要用到这个值,就不需要重复计算了,可以直接使用,所以引出记忆化搜索概念:

记忆化搜索

记忆化搜索其实就是带备忘录的递归

也就相当于递归过程中的剪枝操作了,相当于将一个指数级别的时间复杂度,变为了O(N)级别的,因为只需要计算出一次,剩下分支就可以直接使用,不会进入递归了                                        

所以记忆化搜索的代码实现的步骤就是:

①添加一个备忘录并初始化
②递归每次返回时,将结果放入备忘录中
③每次进入递归时,查看备忘录中是否已经有结果

记忆化搜索代码如下:

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];
    }
};

题目一:不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

这道题在动态规划中做过,这里使用记忆化搜索的方式解决

先想想暴力搜索的写法,再将暴力搜索改为记忆化搜索(要考虑能否改为记忆化搜索)

一、暴力搜索

class Solution 
{
public:
    int uniquePaths(int m, int n) 
    {
        return dfs(m ,n);
    }

    int dfs(int i, int j)
    {
        // 两个边界条件
        if(i == 0 || j == 0) return 0;
        if(i == 1 && j == 1) return 1;

        return dfs(i - 1, j) + dfs(i, j - 1);
    }
};

因为时间复杂度太高了,所以是会超时的

二、改为记忆化搜索

class Solution 
{
public:
    int uniquePaths(int m, int n) 
    {
        // 添加一个备忘录并初始化
        vector<vector<int>> memo(m + 1, vector<int>(n + 1));
        return dfs(m ,n, memo);
    }

    int dfs(int i, int j, vector<vector<int>>& memo)
    {
        // 每次进入递归时,查看备忘录中是否已经有结果
        if(memo[i][j] != 0)
        {
            return memo[i][j];
        }

        if(i == 0 || j == 0) return 0;
        // 递归每次返回时,将结果放入备忘录中
        if(i == 1 && j == 1)
        {
            memo[i][j] = 1;
            return memo[i][j];
        }

        // 递归每次返回时,将结果放入备忘录中
        memo[i][j] = dfs(i - 1, j, memo) + dfs(i, j - 1, memo);
        return memo[i][j];
    }
};

三、改为动态规划

class Solution 
{
public:
    int uniquePaths(int m, int n) 
    {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        dp[1][1] = 1;
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
            {
                if(i == 1 && j == 1) continue;
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        return dp[m][n];

        // 添加一个备忘录并初始化
        // vector<vector<int>> memo(m + 1, vector<int>(n + 1));
        // return dfs(m ,n, memo);
    }
};

题目二:最长递增子序列

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

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

子序列也就是数组中不连续的序列,本题求的就是不连续的递增子序列的最大长度是多少

下面进行暴力搜索、记忆化搜索、改为动态规划三步

一、递归暴力搜索

暴力搜索就是在决策树中,首先确定子序列的起点是哪一个数,依次列举出来,第二层就是根据第一层列举出来的起点数字,从该数往后依次继续列举,直到最后一个数为止

class Solution 
{
public:
    int ret;
    int lengthOfLIS(vector<int>& nums) 
    {
        for(int i = 0; i < nums.size(); i++)
            ret = max(ret, dfs(nums, i));
        return ret;
    }

    int dfs(vector<int>& nums, int pos)
    {
        // 寻找该数字后面的最长子序列,需要加上该数字,所以初始化为1
        int ret = 1;
        for(int i = pos + 1; i < nums.size(); i++)
        {
            // 符合条件再递归
            if(nums[i] > nums[pos])
                ret = max(ret, dfs(nums, i) + 1);
        }
        return ret;
    }
};

本题的暴力搜索同样会超时


二、记忆化搜索

class Solution 
{
public:
    int ret;
    int lengthOfLIS(vector<int>& nums) 
    {
        // 添加一个备忘录并初始化
        int n = nums.size();
        vector<int> memo(n);
        // 哪个位置为最长子序列的起点不确定,所以每个位置都寻找一次
        for(int i = 0; i < n; i++)
            ret = max(ret, dfs(nums, i, memo));
        return ret;
    }

    int dfs(vector<int>& nums, int pos, vector<int>& memo)
    {
        // 每次进入递归时,查看备忘录中是否已经有结果
        if(memo[pos] != 0) return memo[pos];

        // 寻找该数字后面的最长子序列,需要加上该数字,所以初始化为1
        int ret = 1;
        for(int i = pos + 1; i < nums.size(); i++)
        {
            // 符合条件再递归
            if(nums[i] > nums[pos])
                ret = max(ret, dfs(nums, i, memo) + 1);
        }
        // 递归每次返回时,将结果放入备忘录中
        memo[pos] = ret;
        return memo[pos];
    }
};

加上备忘录后,就能运行通过,不会超时了


三、改为动态规划

因为本题也有相同的子问题,会重复计算,例如选择第一个数字作为子序列起点时,会递归到第二个数字,此时就会与初始选择第二个数字做起点重复计算

这里的动态规划版本与动态规划章节里的方式不同,因为此题的动态规划版本是依据记忆化搜索的版本做的改变

因为记忆化搜索中,想知道该位置为起点的最长子序列,需要知道该位置之后的最长子序列,所以是从后向前的

dfs与dp是对应的

class Solution 
{
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        int n = nums.size(), ret = 0;
        // 与记忆化搜索相同,初始化为1
        vector<int> dp(n, 1);
        // 从后向前
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i + 1; j < n; j++)
            {
                if(nums[j] > nums[i])
                    dp[i] = max(dp[i], dp[j] + 1);
            }
            ret = max(ret, dp[i]);
        }
        return ret;

        // 添加一个备忘录并初始化
        // int n = nums.size();
        // vector<int> memo(n);
        // 哪个位置为最长子序列的起点不确定,所以每个位置都寻找一次
        // for(int i = 0; i < n; i++)
        //     ret = max(ret, dfs(nums, i, memo));
        // return ret;
    }
};

题目三:猜数字大小II

我们正在玩一个猜数游戏,游戏规则如下:

  1. 我从 1 到 n 之间选择一个数字。
  2. 你来猜我选了哪个数字。
  3. 如果你猜到正确的数字,就会 赢得游戏 。
  4. 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
  5. 每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。

给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。


猜数字我们都玩过,这道题与传统猜数字不同的是,如果猜错需要支付当前猜的数字的钱

题目要求我们计算出在某一种选择策略中,无论选择的是数字几,都能确保获胜所花的现金数能够获胜,求这些策略中需要的最小的现金数

所以第一步,先考虑暴力搜索怎么做

第一步确定需要选的值是多少,假设该数在 1 ~ 10 之间,此时如果选择 i 

那么可能  i 比所需要的数小,也可能比所需要的数大,所以决策树向左延伸就是 1 ~ i - 1
向右延伸就是 i + 1 ~ 10

由于需要的就是满足条件的最小值,所以在 i 下面扩展的 1 ~ i - 1 与 i + 1 ~ 10中,需要选择最小值向上返回,假设 1 ~ i - 1 区间返回 x,i + 1 ~ 10区间返回 y,那么最后的结果应该是
i + max(x, y),因为最终需要的结果是能够满足当前选择的任意情况,同时还需要加上当前数字的值

也就是上面这种图中,7就表示i,下一层的3和9就表示扩展的1~6和8~10之间选择的一种情况,3返回的是3 + 5 == 8,而9 返回的是 9,所以根节点7为了能够满足这两种情况,最终选择的是9,结果就是 9 + 7 == 16

一、暴力搜索的代码如下:

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

    int dfs(int left, int right)
    {
        if(left >= right) return 0;

        int ret = INT_MAX;
        for(int head = left; head <= right; head++)
        {
            // x和y表示head下面左右分支返回上来的值
            int x = dfs(left, head - 1);
            int y = dfs(head + 1, right);
            ret = min(ret, max(x, y) + head);
        }
        return ret;
    }
};

这种解法肯定是超时的,所以看下面的记忆化搜索代码


 二、改为记忆化搜索

很简单,只需创建备忘录,再在返回时存入与进入递归时查看是否有值即可

只需要加上两行代码,就可以剪枝减掉非常多种情况,因此相比于暴力搜索的代码就不会超时了

代码如下:

class Solution 
{
public:
    int memo[201][201];

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

    int dfs(int left, int right)
    {
        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++)
        {
            // x和y表示head下面左右分支返回上来的值
            int x = dfs(left, head - 1);
            int y = dfs(head + 1, right);
            ret = min(ret, max(x, y) + head);
        }
        memo[left][right] = ret;
        return memo[left][right];
    }
};

题目四:矩阵中的最长递增路径

给定一个 m x n 整数矩阵 matrix ,找出其中 最长递增路径 的长度。

对于每个单元格,你可以往上,下,左,右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外(即不允许环绕)。

示例 1:

输入:matrix = [[9,9,4],[6,6,8],[2,1,1]]
输出:4 
解释:最长递增路径为 [1, 2, 6, 9]

示例 2:

输入:matrix = [[3,4,5],[3,2,6],[2,2,1]]
输出:4 
解释:最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。

示例 3:

输入:matrix = [[1]]
输出:1

本题同样需要先写出暴力解法,就是将每个位置都遍历一遍,再返回计算出的最大的值接口

肯定也会超时的,因为在暴力解法中,每一个位置可能都会计算多次

暴力搜索代码如下:

class Solution {
public:
    int dx[4] = {0,0,-1,1};
    int dy[4] = {-1,1,0,0};
    int ret, m, n;

    int longestIncreasingPath(vector<vector<int>>& matrix) {
        m = matrix.size(), n = matrix[0].size();
        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)
    {
        int ret = 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])
            {
                ret = max(ret, dfs(matrix, x, y) + 1);
            }
        }
        return ret;
    }   
};

更新为记忆化搜索的代码,此时就不会超时了,成功解决问题:

class Solution {
public:
    int dx[4] = {0,0,-1,1};
    int dy[4] = {-1,1,0,0};
    int ret, m, n;
    int memo[201][201];

    int longestIncreasingPath(vector<vector<int>>& matrix) {
        m = matrix.size(), n = matrix[0].size();
        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 ret = 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])
            {
                ret = max(ret, dfs(matrix, x, y) + 1);
            }
        }
        memo[i][j] = ret;
        return memo[i][j];
    }   
};

记忆化搜索相关题目到此结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2050581.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第44课 Scratch入门篇:无限画中画

无限画中画 故事背景: 无止境的显示一幅画。 程序原理: 利用多张基本一样的图,不停循环显示,产生视觉上的错觉,原理很简单,只是一种实现方式而已。 开始编程 1、删除预设的猫咪角色,上传以后在那个无限循环的图片,大小为 480*360 2、接下来复制造型,使用选择工具…

.net 8.0 下 Blazor 通过 SignalR 与 Winform 交互

定义一个Hub using Microsoft.AspNetCore.SignalR;namespace Beatrane.Connect.Blazor {public class DeviceHub : Hub{public async Task SendMessage(string user, string message){await Clients.All.SendAsync("ReceiveMessage", user, message);}public async …

静态分析、动态调试与重打包:去除Android APK烦人广告

最近&#xff0c;一直使用的某款APP&#xff0c;广告越来越多&#xff0c;更令人发指的是&#xff0c;广告弹框最后都变成无法关闭的形式&#xff0c;不使用会员压根没法正常使用。应用市场广大用户的评论说出了我们的心声。 虽说充会员可以免广告&#xff0c;这点小钱&#xf…

《python语言程序设计》2018版第7章第7题代数2x2线性方程式设计一个名为LinearEquation

#大家可以看一下 两道题的内容 第n次刷第4章第3题的代码。朝纲用来函数的概念 def judge_num(a, b, c, d):return (a * d) - (b * c)def run_cont(a, b, c, d, e, f):cc judge_num(a, b, c, d)if cc 0:print("The equation has no solution")else:x ((e * d) - (…

苹果手机怎么清理重复照片的解决方案

随着智能手机摄像头技术的飞速发展&#xff0c;我们越来越依赖iPhone来记录生活中的点点滴滴。不可避免地&#xff0c;这也导致了大量重复照片的产生&#xff0c;这些重复照片不仅占用了宝贵的存储空间&#xff0c;还使得照片库显得混乱无序。本文将介绍苹果手机怎么清理重复照…

微信小程序开发的强大助力:HTTP 虚拟专线

​编辑 一、微信小程序开发的热潮与挑战 二、HTTP 虚拟专线的引入 三、HTTP 虚拟专线的关键功能 &#xff08;一&#xff09;用于回调 &#xff08;二&#xff09;助力运维 四、HTTP 虚拟专线的技术优势 &#xff08;一&#xff09;80 和 443 端口的灵活访问 &#xff0…

测试架构师技能修炼---关系化透明

目录 一、该信任时就给别人信任 二、你说的话长久不变 三、道歉表明你的透明化 四、学会在做出反应前倾听 五、允许别人对你透明化 它涉及与别人之间的关系应保持透明化&#xff0c;包括给别人信任&#xff1b;持续给别人传递一致的消息&#xff1b;向别人道歉&#xff1b…

[SDK]-键盘消息和鼠标消息

前言 各位师傅大家好&#xff0c;我是qmx_07&#xff0c;今天给大家讲解键盘消息和鼠标消息&#xff0c;下一节讲解控件的相关知识点 键盘消息 应用程序从windows接收的关于键盘事件的消息分为击键和字符两种windows再发送击键消息的同时 还会发送字符消息Shift、ctrl、alt…

【C++】string讲解

一、string的理解 我们可以把string看作一个更高级用类实现的char* 。或者直接叫他字符串类型&#xff0c;一听就是定义字符串的。 二、string的使用 用法就和int、char 类型一样&#xff0c;而且功能比他们强大很多。 三、string的功能 只列举常用功能 1、通过“[]”访问…

日撸Java三百行(day26:栈实现二叉树深度遍历之前后序遍历)

目录 一、栈实现前序遍历 二、栈实现后序遍历 三、完整的程序代码 总结 一、栈实现前序遍历 先来看看我们之前写的用递归实现前序遍历的程序代码&#xff1a; /************************ Pre-order visit.**********************/public void preOrderVisit() {System.out…

Simple RPC - 06 从零开始设计一个服务端(上)_注册中心的实现

文章目录 Pre核心内容服务端结构概述注册中心的实现1. 注册中心的架构2. 面向接口编程的设计3. 注册中心的接口设计4. SPI机制的应用 5. 小结 Pre Simple RPC - 01 框架原理及总体架构初探 Simple RPC - 02 通用高性能序列化和反序列化设计与实现 Simple RPC - 03 借助Netty…

【与C++的邂逅】--- 类和对象(上)

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 与C的邂逅 本篇博客将讲解C中的类和对象&#xff0c;C是面向对象的语言&#xff0c;面向对象三大特性是封装,继承,多态。学习类和对象&#xff0c;我们可…

Adobe Dimension DN v4.0.2 解锁版下载和安装教程 (专业的三维3D建模工具)

前言 Adobe Dimension&#xff08;简称DN&#xff09;是一款3D设计软件&#xff0c;三维合成和渲染工具&#xff0c;2D平面的二维转为3D立体的三维合成工具&#xff0c;用于3Dmax\C4D\MAYA等三维软件生成的效果图&#xff0c;在3D场景中排列对象、图形和光照。3D应用程序使用的…

Nginx实验

编译安装 Nginx 准备rhel9环境 下载安装包nginx-1.24.0&#xff08;xftp&#xff09;/复制下载链接 &#xff08;nginx.org——>download&#xff09; 解压 [rootnginx nginx-1.24.0]# tar zxf nginx-1.24.0.tar.gz [rootnginx nginx-1.24.0]#tar zxf nginx-1.24.0.tar.…

yolov8安装教程

一、资源下载 1.下载YOLOv8代码 github:YOLOv8-github gitee:YOLOv8-gitee&#xff08;推荐使用国内的gitee&#xff09; 2.conda、cuda 如果没有安装conda&#xff0c;按照流程安装好conda&#xff0c;还要下载好符合自己电脑版本的CUDA 后续会用。 二、创建conda虚拟环…

C语言典型例题43

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 习题3.3 有一个函数&#xff1a;y{x,x<1;2x-1,1≤x≤10;3x-11,x≥10。写程序&#xff0c;输入x&#xff0c;输出y。 代码&#xff1a; //《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 //习题3.3…

OD C卷 - 传递悄悄话

传递悄悄话 &#xff08;100&#xff09; 给定一个二叉树&#xff0c;节点采用顺序存储&#xff0c;如 i0 表示根节点&#xff0c;2i 1 表示左子树根&#xff0c;2i 2 表示右子树根;每个节点站一个人&#xff0c;节点数值表示由父节点到该节点传递消息需要的时间&#xff1b…

周末休整

我写的东西&#xff0c;不爱看的人可以不看&#xff0c;我是给喜欢我的人写的&#xff0c;不喜欢我的人&#xff0c;我也讨厌她。 今天故意写点教人学坏的东西&#xff0c;因为以前写了很多正能量的东西&#xff0c;虽然阅读量还可以&#xff0c;但当见面聊天之后&#xff0c;…

【CSS】CSS新单位vmin和vmax

通过vmin单位可以自动取视口宽度和高度中较小的那个值&#xff0c;vmax同理。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1…

百度智能云通用文字识别(标准版)- java.lang.NoSuchFieldError: Companion

需求环境 ORC识别图片信息 参考百度示例 百度智能云API文档通用文字识别 官方示例 package baidu.com;import okhttp3.*; import org.json.JSONObject;import java.io.*;/*** 需要添加依赖* <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->…