递归、搜索与回溯算法(专题六:记忆化搜索)

news2024/9/24 18:04:36

目录

1. 什么是记忆化搜索(例子:斐波那契数)

1.1 解法一:递归

1.2 解法二:记忆化搜索

1.2.1 记忆化搜索比递归多了什么?

1.2.2 提出一个问题:什么时候要使用记忆化搜索呢?

1.3 解法三:动态规划

1.3.1 先复习一下动态规划的核心步骤(5个),并将动态规划的每一步对应记忆化搜索(加强版的递归)的每一步

1.3.2 通过上面的解析,发现一个特点

1.3.3 动态规划 and 记忆化搜索的本质 

补充

2. 题目

2.1  不同路径(medium)

2.1.1 递归解法

2.1.2 记忆化搜索解法

2.1.3 动态规划解法 

2.2 最长递增子序列

2.2.1 递归解法

2.2.2 记忆化搜索解法

2.2.3 动态规划解法 

2.3 猜数字大小 Ⅱ

2.3.1 递归解法

2.3.2 记忆化搜索解法

2.4 矩阵中的最长递增路径

2.4.1 递归解法

2.4.2 记忆化搜索解法


1. 什么是记忆化搜索(例子:斐波那契数)

力扣题目链接

记过前面几篇文章中,我介绍了什么递归、搜索和回溯,以及他们之间的关系。接下来我们进阶一下,来一起看看什么是记忆化搜索,看看记忆化搜索与递归,乃至动态规划算法之间有什么联系吧。

我打算用一道很经典的例题,分享一下什么是记忆化搜索,这道题就是斐波那契数!

斐波那契数的解法有很多(①循环;②递归;③动态规划;④记忆化搜索;⑤矩阵快速幂),在这几种解法中,矩阵快速幂的时间复杂度是最小的。

关于矩阵快速幂,我会在接下来的文章中分享给大家,敬请期待!!!

题目如下:

解法分析:

1.1 解法一:递归

递归解决这题会出现的问题:

① 进行了很多重复性的计算,例如fib(3)、fib(2)都计算了多次,这就大大增加运算时间,时间复杂度为O(2^n)。

② 如果 n 太大,fib(n)的执行可能还会导致栈溢出

    //第一种方法:递归
    public int fib1(int n) {
        if(n == 0) return 0;
        if(n == 1) return 1;

        return fib(n - 1) + fib(n - 2);
    }

我们可以发现,递归是可以执行通过的。这道题给的测试用例都是比较少,所以不会导致超时的现象。

1.2 解法二:记忆化搜索

1.2.1 记忆化搜索比递归多了什么?

答:比递归多了一个“备忘录”的功能(加强版的递归)。在上面的第一种解法有提到递归的缺点就是有事会进行大量的重复计算,导致时间复杂度过大。“备忘录”就是用来存储每次递归的结果,如果在另一个分支中有进行一样的运算,就不需要再进行递归展开了,只要从“备忘录”中将值取出来直接返回即可。具体看下图:

首先我们创建一个备忘录,例如:int memo[n];

如何实现记忆化搜索?

① 添加一个备忘录。

② 递归每次返回时,将结果放到备忘录里面。

③ 在每次进入递归之前,先往备忘录里瞅一瞅,看看是否已经存在了 。

    int[] memo;//作为备忘录
    //第二种方法:记忆化搜索
    public int fib(int n) {
        //初始化
        memo = new int[n + 1];
        Arrays.fill(memo, -1);
        //进行记忆化深搜
        return dfs(n);
    }

    int dfs(int n){
        if(memo[n] != -1) return memo[n];
        if(n <= 1){
            memo[n] = n;
            return n;
        }
        memo[n] = dfs(n - 1) + dfs(n - 2);
        return memo[n];
    }

发现了一个有趣的结果,使用递归解这道题花了10ms,而使用记忆化搜索只要0ms!(虽然力扣的执行时间不是很严谨,但也可以一看) 

记忆化搜索,省略了很多重复计算的步骤,所以时间复杂度大大减少了,为 O(n)。

1.2.2 提出一个问题:什么时候要使用记忆化搜索呢?

答:① 只有当一个道题有许多重复计算,换句话说,有许多重复的子问题时,可以使用记忆化搜索来降低时间复杂度。② 如果没有许多重复计算,换句话说递归展开图只是一棵单分支树,就没必要用记忆化搜索了。

1.3 解法三:动态规划

1.3.1 先复习一下动态规划的核心步骤(5个),并将动态规划的每一步对应记忆化搜索(加强版的递归)的每一步

① 确定动态表示:dp[i]要表示什么,dp[i]表示第i位的斐波那契数 ——> 递归:dfs函数的含义(函数头有什么参数、什么返回值)

② 确定动态转移方程:dp[i] = dp[i - 1] + dp[i - 2] ——> 递归:dfs函数的主体(函数做了什么)

③ 初始化:防止越界,dp[0] = 0,dp[1] = 1 ——> 递归:dfs函数的递归出口(n == 0 或 n == 1时)

④ 确定填表顺序:从左往右 ——> 递归:填写备忘录的顺序

⑤ 确定返回值:dp[n] ——> 递归:主函数如何调用dfs函数
 

    int[] dp;
    public int fib(int n) {
        //1.对dp初始化
        dp = new int[n + 1];
        dp[0] = 0;
        if(n > 0)
            dp[1] = 1;
        //2.开始填表
        for(int i = 2;i <= n;i++){
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        //3.返回值
        return dp[n];
    }

1.3.2 通过上面的解析,发现一个特点

记忆化搜索是进行自顶向下计算,动态规划是进行自底向上计算。

1.3.3 动态规划 and 记忆化搜索的本质 

① 都是暴力解法,一一枚举

② 都是将计算好的结果存储起来

③ 记忆化搜索(递归的形式),动态规划(递推的形式,利用循环)

补充

(1)带备忘录的递归 vs 带备忘录的动态规划 vs 记忆化搜索 三者都是一回事,就是记忆化搜索或者说动态规划。

(2)能用暴搜解的题,一般可以改成记忆化搜索,但不一定可以改成动态规划。暴搜的本质是给动态规划提供一个“填表方向”。

经过上面的分享,大家应该对递归、记忆化搜索和动态规划有了一个新的了解,接下来通过做题来巩固加深我们的知识体系吧。

2. 题目

2.1  不同路径(medium)

力扣题目链接

 解析:

2.1.1 递归解法

    //第一种:递归解法
    public int uniquePaths(int m, int n) {
        if(m == 0 || n == 0) return 0;
        if(m == 1 && n == 1) return 1;
        return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
    }

 在运行的时候是可以运行通过,但是当 m 和 n 太大了呢?我们来看看提交会发生什么。

超时!!! m和n一大就发什么超时现象,接下来看看用记忆化搜索又怎么样。

2.1.2 记忆化搜索解法

    //第二种:记忆化搜索
    public int uniquePaths(int m, int n) {
        int[][] memo = new int[m + 1][n + 1];
        return dfs(m,n,memo);
    }
    int dfs(int m,int n,int[][] memo) {
        if(m == 0 || n == 0) return 0;
        if(memo[m][n] != 0) return memo[m][n];
        if(m == 1 && n == 1){
            memo[m][n] = 1;
            return 1;
        }
        memo[m][n] = dfs(m-1,n,memo) + dfs(m,n-1,memo);
        return memo[m][n];
    }

此时提交就可以通过了,不会发生超时。

2.1.3 动态规划解法 

(1)确定动态表示:dp[i][j] 为 到当前位置有多少种路径。

(2)状态转移方程:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。

(3)初始化:dp[0][j] = dp[i][0] = 0,dp[1][1] = 1。

(4)填表顺序:从左往右,从上往下。

(5)返回值:return dp[m][n]。

    // 第三种:动态规划解法
    public int uniquePaths(int m, int n) {
        //1.创建dp表
        int[][] dp = new int[m + 1][n + 1];
        //2和3一起,初始化和填表
        dp[1][1] = 1;
        for(int i = 1;i < m + 1;i++){
            for(int j = 1;j < n + 1;j++){
                if(i == 1 && j == 1) continue;//到(1,1)位置的路径都是1,不用再修改了
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        //4.返回值
        return dp[m][n];
    }

2.2 最长递增子序列

力扣题目链接

 解析:

2.2.1 递归解法

    public int lengthOfLIS(int[] nums){
        int ret = 0;
        for(int i = 0;i < nums.length;i++){
            ret = Math.max(ret,dfs(nums,i));
        }
        return ret;
    }
    public int dfs(int[] nums,int pos){
        int ret = 1;
        for(int i = pos + 1;i < nums.length;i++){
            if(nums[pos] < nums[i])
                ret = Math.max(ret,dfs(nums,i) + 1);
        }
        return ret;
    }

我们可以发现一些测试用例是可以通过,但是当数组的大小过于大时,就会报超时的错误!!!

对于这种情况,可以将代码改成记忆化搜索或者动态规划,用空间换取时间,减小时间复杂度!!!

2.2.2 记忆化搜索解法

例如这种情况:在pos的第一个分支,pos+1已经计算过了,则在根结点的第二个分支就不需要在此重复计算pos+1了。

    int[] memo;

    public int lengthOfLIS(int[] nums){
        memo = new int[nums.length];
        int ret = 0;
        for(int i = 0;i < nums.length;i++){
            ret = Math.max(ret,dfs(nums,i));
        }
        return ret;
    }
    public int dfs(int[] nums,int pos){
        if(memo[pos] != 0){
            return memo[pos];
        }
        int ret = 1;
        for(int i = pos + 1;i < nums.length;i++){
            if(nums[pos] < nums[i])
                ret = Math.max(ret,dfs(nums,i) + 1);
        }
        memo[pos] = ret;
        return ret;
    }

2.2.3 动态规划解法 

(1)确定动态表示:dp[i] 表示从第i个位置开始,符合子序列条件的子序列长度为多少

(2)状态转移方程:当后一个元素大于前一个元素,有 dp[i] = Math.max(dp[i],dp[j] + 1); 

(3)初始化:Arrays.fill(dp,1); 所有的位置都是1,最糟糕的情况就是该位置的元素自己,所以就是1。

(4)填表顺序:从右往左。其实就是从少到多的过程。

(5)返回值:ret = Math.max(dp[i],ret); 看从哪个位置开始,能得到最长的子序列。

    //第三种:动态规划
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length + 1];
        int ret = 0;
        Arrays.fill(dp,1);
        for(int i = nums.length - 1;i >= 0;i--){
            for(int j = i + 1;j < nums.length;j++){
                if(nums[i] < nums[j]){
                    dp[i] = Math.max(dp[i],dp[j] + 1);
                }
            }
            ret = Math.max(dp[i],ret);
        }
        return ret;
    }

2.3 猜数字大小 Ⅱ

力扣题目链接

解析:

2.3.1 递归解法

    //第一种方法:暴搜
    public int getMoneyAmount(int n) {
        return dfs1(1,n);
    }
    int dfs1(int left,int right){
        /*
        当left == right证明已经找到该数,所以就不需要支付费用
        当1作为根结点,则有 [left,head - 1] == [1,0],这种情况不存在也
        要返回0,因为不存在,所以不会有消耗
        */
        if(left >= right) return 0;
        int ret = Integer.MAX_VALUE;
        for(int head = left;head <= right;head++){
            //x是用来找左子树的值
            int x = dfs1(left,head - 1);
            //y是用来找右子树的值
            int y = dfs1(head + 1,right);
            ret = Math.min(Math.max(x,y)+head,ret);
        }
        return ret;
    }

 这种超时报错,我们在上面的例题中已经遇到了许多次,所以我们将代码改为记忆化搜索

2.3.2 记忆化搜索解法

可以发现在选择5作为根结点时,出现了[6, 10]的区间;在选择3作为根结点时,也出现了[6, 10]的区间,这部分就导致了重复计算。所以我们将每个区间的结果保存起来,减少时间复杂度。

    //第二种方法:记忆化搜索
    int[][] memo;
    public int getMoneyAmount(int n) {
        memo = new int[n + 1][n + 1];
        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 = Integer.MAX_VALUE;
        for(int head = left;head <= right;head++){
            int x = dfs(left,head - 1);
            int y = dfs(head + 1,right);
            ret = Math.min(Math.max(x,y)+head,ret);
        }
        memo[left][right] = ret;
        return ret;
    }

 

2.4 矩阵中的最长递增路径

力扣题目链接

 解析:

2.4.1 递归解法

算法思路:暴搜
a. 递归含义:给 dfs ⼀个使命,给他⼀个下标 [i, j] ,返回从这个位置开始的最⻓递增路径
的⻓度;
b. 函数体:上下左右四个⽅向瞅⼀瞅,哪⾥能过去就过去,统计四个⽅向上的最⼤⻓度;
c. 递归出⼝:因为我们是先判断再进⼊递归,因此没有出⼝~

    //方向的选择:具体看上图
    int[] dx = {-1,0,1,0};
    int[] dy = {0,1,0,-1};
    int m,n;
    public int longestIncreasingPath(int[][] matrix) {
        m = matrix.length;
        n = matrix[0].length;
        int ret = 0;
        for(int i = 0;i < m;i++){
            for(int j = 0;j < n;j++){
                ret = Math.max(ret,dfs(matrix,i,j));
            }
        }
        return ret;
    }
    public int dfs(int[][] matrix,int i,int j){
        int ret = 1;
        //从四个方向进行暴搜
        for(int k = 0;k < 4;k++){
            int x = i + dx[k],y = j + dy[k];
            if(x >= 0 && x < m && y >= 0 && y < n && matrix[x][y]
            > matrix[i][j]){
                ret = Math.max(ret,dfs(matrix,x,y) + 1);
            }
        }
        return ret;
    }

 这种超时报错,我们在上面的例题中已经遇到了许多次,所以我们将代码改为记忆化搜索。

2.4.2 记忆化搜索解法

注:这道题的递归图为什么会重复就不给小伙伴们画了,大家可以动手画一画,想想为什么会有重复计算?

int[] dx = {-1,0,1,0};
    int[] dy = {0,1,0,-1};
    int[][] memo;//“备忘录”
    int m,n;
    public int longestIncreasingPath(int[][] matrix) {
        m = matrix.length;
        n = matrix[0].length;
        memo = new int[m][n];
        int ret = 0;
        for(int i = 0;i < m;i++){
            for(int j = 0;j < n;j++){
                ret = Math.max(ret,dfs(matrix,i,j));
            }
        }
        return ret;
    }
    public int dfs(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],y = j + dy[k];
            if(x >= 0 && x < m && y >= 0 && y < n && matrix[x][y]
            > matrix[i][j]){
                ret = Math.max(ret,dfs(matrix,x,y) + 1);
            }
        }
        memo[i][j] = ret;//每次结果保存到备忘录
        return ret;
    }

 

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

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

相关文章

运维平台介绍:视频智能运维平台的视频质量诊断分析和告警中心

目 录 一、视频智能运维平台介绍 &#xff08;一&#xff09;平台概述 &#xff08;二&#xff09;结构图 &#xff08;三&#xff09;功能介绍 1、运维监控 2、视频诊断 3、巡检管理 4、告警管理 5、资产管理 6、工单管理 7、运维…

如何在Linux上部署1Panel面板并远程访问内网Web端管理界面

在Linux环境中部署1Panel面板&#xff0c;并实现安全的远程访问是一种高效管理服务器资源的方式。下面是如何实现这一目标的详细步骤。 1Panel面板的优势 易用性&#xff1a;1Panel提供了图形化的界面&#xff0c;使得非专业人士也能轻松管理服务器。 功能丰富&#xff1a;它…

3D Gaussian Splatting:论文原理分析

标题&#xff1a;3D Gaussian Splatting for Real-Time Radiance Field Rendering 作者&#xff1a;Bernhard Kerbl、Georgios Kopanas、Thomas Leimkhler和George Drettakis&#xff0c;来自法国Inria、Universit Cte dAzur和德国Max-Planck-Institut fr Informatik。 发表时…

mysql 为大表新增字段或索引

1 问题 mysql 为大表增加或增加索引等操作时&#xff0c;直接操作原表可能会因为执行超时而导致失败。解决办法如下。 2 解决办法 &#xff08;1&#xff09;建新表-复制表A 的数据结构&#xff0c;不复制数据 create table B like A; &#xff08;2&#xff09;加字段或索…

聚类算法(KMeans)模型评估方法(SSE、SC)及案例

一、概述 将相似的样本自动归到一个类别中&#xff0c;不同的相似度计算方法&#xff0c;会得到不同的聚类结果&#xff0c;常用欧式距离法&#xff1b;聚类算法的目的是在没有先验知识的情况下&#xff0c;自动发现数据集中的内在结构和模式。是无监督学习算法 二、分类 根据…

vue3开发移动端H5页面中video无交互自动播放完美解决方案

链接 官网&#xff1a;https://jsmpeg.com/ github&#xff1a;https://github.com/phoboslab/jsmpeg 官方例子&#xff1a;https://jsmpeg.com/perf.html 在线video转ts文件&#xff1a;https://convertio.co/zh/mp4-ts/ 踩坑 一、不用使用任何npm、yarn等安装 npm i jsmpe…

C#用Math.Round和double.TryParse方法实现四舍五入

目录 一、涉及到的知识点 1.double.TryParse&#xff08;&#xff09;方法 2.Math.Round(Decimal, Int32) 方法 3.comboBox1没有选项 二、示例 1.源码 2.生成 一、涉及到的知识点 1.double.TryParse&#xff08;&#xff09;方法 详见本文作者写的其他文章&#xff0…

消息中间件之Kafka(一)

1.简介 高性能的消息中间件&#xff0c;在大数据的业务场景下性能比较好&#xff0c;kafka本身不维护消息位点&#xff0c;而是交由Consumer来维护&#xff0c;消息可以重复消费&#xff0c;并且内部使用了零拷贝技术&#xff0c;性能比较好 Broker持久化消息时采用了MMAP的技…

像操作本地文件一样操作linux文件 centos7环境下samba共享服务搭建详细教程

1.安装dnf yum -y install dnf 2.安装samba dnf install samba -y 3.配置 3.1创建并设置用户信息 #创建用户 useradd -M -s /sbin/nologin samba echo 123|passwd --stdin samba mkdir /home/samba chown -R samba:samba /home/samba smbpasswd -a samba smaba设置密码示…

nodejs下载安装

一、node下载安装 官网下载 官网 根据自己电脑系统选择合适的版本进行下载&#xff0c;我这里选择window 64 位 下载完点击安装 打开cmd查看安装 此处说明下&#xff1a;新版的Node.js已自带npm&#xff0c;安装Node.js时会一起安装&#xff0c;npm的作用就是对Node.js…

实现仿ChatGPT光标跟随效果

先看效果 实现效果 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>光标闪烁效果</title>…

使用 MinIO 和 PostgreSQL 简化数据事件

本教程将教您如何使用 Docker 和 Docker Compose 在 MinIO 和 PostgreSQL 之间设置和管理数据事件&#xff0c;也称为存储桶或对象事件。 您可能已经在利用 MinIO 事件与外部服务进行通信&#xff0c;现在您将通过使用 PostgreSQL 自动化和简化数据事件管理来增强数据处理能力…

机器人导纳控制实现框架

Safe, Stable and Intuitive Control for Physical Human-Robot Interaction - 知乎关于文章《Safe, Stable and Intuitive Control for Physical Human-Robot Interactio》的简记。 Safe, Stable and Intuitive Control for Physical Human-Robot Interaction目的根据力导数作…

设计一个网页爬虫

定义 User Case 和 约束 注意&#xff1a;没有一个面试官会阐述清楚问题&#xff0c;我们需要定义Use case和约束 Use cases 我们的作用域只是处理以下Use Case&#xff1a; Service 爬取一批 url 生成包含搜索词的单词到页面的反向索引给页面生成标题和片段– 标题和片段是…

ptrade 通过mysql的链接开发一个量化管理平台。

这里只写一下界面及想法。不进行代码的实现。因为对流程不是很熟 ###界面 数据库的链接&#xff1a; ptrade USER 可转债量化分析 PASSWORD 123456 MYSQL_HOST mysql.sqlpub.com MYSQL_PORT 3306 MYSQL_DB ptradedef get_mysql_conn():import pymysqltry:conn pym…

maven编译时依赖报错 Caused by: java.util.zip.ZipException: zip file is empty 错误。

出现这种报错时&#xff0c;可能是maven仓库下对应的依赖出现了问题&#xff0c;需要讲报错依赖位置的依赖进行删除&#xff0c;在编译的时候就会重新下载&#xff0c;就不会出现错误了。 rm -rf /Applications/software/env/repository/org/apache/orc/orc-core/1.9.1/

Yield Guild Games 宣布与区块链游戏中心 Iskra 建立战略合作伙伴关系

Yield Guild Games (YGG) 宣布将向 Iskra 引入其任务系统&#xff0c;Iskra 是一个 Web3 游戏中心和发布平台&#xff0c;拥有超过 400 万注册钱包和 10 万月度活跃用户 (MAU)。在 LINE、Kakao、Wemade 和 Netmarble 等公司的支持下&#xff0c;Iskra 将游戏玩家和游戏工作室聚…

验收测试的重要性:确保交付高质量产品

在软件开发生命周期中&#xff0c;验收测试扮演着至关重要的角色&#xff0c;它不仅是项目的最后一道关卡&#xff0c;更是确保交付高质量产品的关键步骤。本文将介绍验收测试的重要性&#xff0c;以及它在软件开发过程中的作用。 1. 确认功能符合需求 验收测试的首要任务是验证…

逆向思维,去重Cube计算优化新技巧

场景描述 在做数据汇总计算和统计分析时&#xff0c;最头疼的就是去重类指标计算&#xff08;比如用户数、商家数等&#xff09;&#xff0c;尤其还要带多种维度的下钻分析&#xff0c;由于其不可累加的特性&#xff0c;几乎每换一种统计维度组合&#xff0c;都得重新计算。数…

匿名发送短信

匿名发送短信 匿名发送短信啦&#xff01;不用程序猿&#xff0c;也能定制专属推送消息&#xff01;每日小惊喜&#xff01; 还可以领取课程资料&#xff01;软考中级软件设计师&#xff0c;高级信息系统项目管理师&#xff01; 先说在哪里 微信搜公众号&#xff1a;暮看云 微…