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

目录

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.mzph.cn/news/634373.shtml

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

相关文章

第十三章 MySQL

第十三章 MySQL 下面是创建数据库操作 删除数据库 右上角选择要操作的数据库 如果关闭了这个控制台&#xff0c;下次如何找到它呢 也可以对其改名

Linux环境下,针对QT软件工程搭建C++Test单元测试环境的操作指南

文章目录 前言一、安装QT二、安装CTest三、使用QT生成.bdf文件四、创建CTest工程注意事项 前言 CTest是Parasoft公司出品的一款可以针对C/C源代码进行静态分析、单元测试、集成测试的测试工具。本文主要讲解如何在Linux环境下&#xff0c;搭建QT插件版的CTest测试环境。 一、…

【b站咸虾米】chapter4_vue组件_新课uniapp零基础入门到项目打包(微信小程序/H5/vue/安卓apk)全掌握

课程地址&#xff1a;【新课uniapp零基础入门到项目打包&#xff08;微信小程序/H5/vue/安卓apk&#xff09;全掌握】 https://www.bilibili.com/video/BV1mT411K7nW/?p12&share_sourcecopy_web&vd_sourceb1cb921b73fe3808550eaf2224d1c155 四、vue组件 uni-app官网 …

RabbitMQ的基本使用,进行实例案例的消息队列

目录 一、介绍 1. 概述 2. 作用 3. 工作原理 二、RabbitMQ安装部署 1. 安装 2. 部署 3. 增加用户 三、实现案例 1. 项目创建 2. 项目配置 3. 生产者代码 4. 消费者代码 四、测试 每篇一获 一、介绍 1. 概述 RabbitMQ 是一种开源的消息代理和队列服务器&#x…

Spring Web文件上传功能简述

文章目录 正文简单文件上传文件写入 总结 正文 在日常项目开发过程中&#xff0c;文件上传是一个非常常见的功能&#xff0c;当然正规项目都有专门的文件服务器保存上传的文件&#xff0c;实际只需要保存文件路径链接到数据库中即可&#xff0c;但在小型项目中可能没有专门的文…

Oracle命令大全

文章目录 1. SQL*Plus命令&#xff08;用于连接与管理Oracle数据库&#xff09;2. SQL数据定义语言&#xff08;DDL&#xff09;命令3. SQL数据操作语言&#xff08;DML&#xff09;命令4. PL/SQL程序块5. 系统用户管理6. 数据备份与恢复相关命令1. SQL*Plus命令&#xff08;用…

西门子燃烧控制器维修LMV37.410A2WH

西门子燃烧控制器维修范围包括&#xff1a; LMV系列燃烧器控制系统维修 LMV5系列控制器维修 AZL系列显示操作单元维修 QRI系列火焰探测器维修 SQM4系列电动执行机构维修 AZL系列或其他控制系统维修或设置燃烧器的启停&#xff0c;燃料&#xff0c;运行模式&#xff0c;运行…

基于网络爬虫的微博热点分析,包括文本分析和主题分析

基于Python的网络爬虫的微博热点分析是一项技术上具有挑战性的任务。我们使用requests库来获取微博热点数据&#xff0c;并使用pandas对数据进行处理和分析。为了更好地理解微博热点话题&#xff0c;我们采用LDA主题分析方法&#xff0c;结合jieba分词工具将文本分割成有意义的…

配置zabbix平台对数据库以及主从状态的监控

引言&#xff1a;明人不说暗话&#xff0c;今天分享下配置zabbix平台对数据库以及主从状态的监控 准备好zabbix监控平台&#xff08;zabbix-server端&#xff09;例10.12.153.235 db1客户端&#xff08;zabbix-agent&#xff09;例10.12.153.73 1.安装Zabbix存储库 # rpm -Uv…

PDF.js - 免费开源的 JavaScript 读取、显示 PDF 文档的工具库,由 Mozilla 开发并且持续维护

最近新项目需要处理 PDF&#xff0c;研究了 PDf.js 之后觉得很不错&#xff0c;于是写篇文章推荐给大家。 PDF.js 的功能和它的名字一样简单&#xff0c;是一个使用 HTML5 技术来让前端网页支持读取、解析和显示 PDF 文档的 JS 工具库。这个项目由大名鼎鼎的 Mozilla 组织开发…

RabbitMQ安装和使用

简介 RabbitMQ是一套开源&#xff08;MPL&#xff09;的消息队列服务软件&#xff0c;是由LShift提供的一个Advanced Message Queuing Protocol (AMQP) 的开源实现&#xff0c;由以高性能、健壮以及可伸缩性出名的Erlang写成。所有主要的编程语言均有与代理接口通讯的客户端库…

SQL Server中数据表的增删查改

文章目录 一、增二、查三、改四、删除 一、增 进行增删查改的前提需要在指定数据库中创建数据表&#xff0c;对这块不大理解的可以先看看前面几期文章&#xff1a; 创建数据库 创建数据表 use StudentManageDB go insert into Students (StudentName,Gender,Birthday,Age,Stu…

HarmonyOS【应用服务开发】在模块中添加Ability

Ability是应用/服务所具备的能力的抽象&#xff0c;一个Module可以包含一个或多个Ability。应用/服务先后提供了两种应用模型&#xff1a; FA&#xff08;Feature Ability&#xff09;模型&#xff1a; API 7开始支持的模型&#xff0c;已经不再主推。Stage模型&#xff1a;AP…

C# new Thread和Task.Run,多线程(Thread和Task)

一、开启多线程-new Thread的使用 示例一 Thread thread25yi new Thread(new ThreadStart(obj.MethodTimer1)); thread25yi.Start(); void MethodTimer1() { while (true) { Console.WriteLine(DateTime.Now.ToString() "_" thread25yi.CurrentThread.Managed…

杂记 | 在Linux上使用Docker-compose安装单机版Milvus向量数据库并配置访问控制和可视化面板(Attu)

文章目录 01 Milvus向量数据库简介02 安装前的准备03 安装3.1 创建milvus工作目录3.2 下载并编辑docker-compose.yml3.3 下载milvus.yml文件3.4 启动milvus 04 访问可视化面板并修改密码 01 Milvus向量数据库简介 Milvus是一款开源的向量数据库&#xff0c;它专为AI应用设计&a…

Docker(三)使用 Docker 镜像:从仓库获取镜像;管理本地主机上的镜像;介绍镜像实现的基本原理

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; 使用 Docker 镜像 在之前的介绍中&#xff0c;我们知道镜像是 Docker 的三大组件之一。 Docker 运行容器前需要本地存在对应的镜像&#x…

IaC基础设施即代码:Terraform 使用 dynamic动态内联块 创建docker资源

目录 一、实验 1.环境 2.Terraform查看版本 3.Linux主机安装Docker 4.Terraform使用本地编译&#xff08;In-house&#xff09;的Providers 5.Docker-CE 开启远程API 6. Linux主机拉取镜像 7.Terraform 使用 dynamic动态内联块 创建资源 二、问题 1.Terraform 计划资源…

精品基于Uniapp+springboot校园学校趣事管理系统app

《[含文档PPT源码等]精品基于Uniappspringboot趣事管理系统app》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;Java 后台框架&#xff1a;springboot、ssm 安卓…

Unity vs Godot :哪个游戏引擎更适合你?

Unity vs Godot &#xff1a;哪个游戏引擎更适合你&#xff1f; 游戏引擎的选择对开发过程和最终产品质量有着重大影响。近年来&#xff0c;Godot和Unity这两款引擎受到广泛关注。本文将从多个维度对两者进行比较&#xff0c;以期为开发者提供正确的选择建议。 Godot和Unity都有…

Backtrader 文档学习-Indicators混合时间框架

Backtrader 文档学习-Indicators混合时间周期 1.不同时间周期 如果数据源在Cerebro引擎中具有不同的时间范围和不同的长度&#xff0c;指示器将会终止。 比如&#xff1a;data0是日线&#xff0c;data1是月线 。 pivotpoint btind.PivotPoint(self.data1) sellsignal self…