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

目录

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.hqwc.cn/news/415786.html

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

相关文章

腾讯二面:自动贩卖机/音频播放器使用了什么设计模式?

状态模式是什么&#xff1f; 状态模式&#xff0c;也被称作状态对象模式&#xff0c;是一种行为设计模式。 当一个对象的内在状态改变时&#xff0c;允许改变其行为&#xff0c;这个对象看起来像是改变了其类。 它让对象在其内部状态改变时改变自己的行为。外部调用者无需了…

栈、队列专题

文章目录 栈栈的概述栈的实现栈在函数调用中的应用栈在表达式求值中的应用逆波兰表达式求值 栈在括号匹配中的应用有效的括号最长的有效括号删除字符串中的所有相邻重复项 如何获取栈内最小元素呢如何实现浏览器的前进和后退 队列队列的定义队列的实现循环队列队列的应用队列在…

16.桥接模式

桥接模式 介绍 桥接模式是一种结构型设计模式&#xff0c;它通过将抽象部分与实现部分分离&#xff0c;使它们可以独立变化。这种模式通过组合的方式来实现&#xff0c;而不是继承。桥接模式通过将抽象和实现解耦&#xff0c;从而实现抽象和实现的分离&#xff0c;使得系统更加…

c++调用函数传值传参

先回顾一下上次我的指针博客 今天看书时候发现一个东西看不懂,不懂就查&#xff0c;不能一直半解 int *find(const vector<int> &vec,int value){ //省略....... } 和 elemType* find(const elemType * array,int size,const elemType &value){ //省略.........…

C#,入门教程(07)——软件项目的源文件与目录结构

上一篇&#xff1a; C#&#xff0c;入门教程(06)——解决方案资源管理器&#xff0c;代码文件与文件夹的管理工具https://blog.csdn.net/beijinghorn/article/details/124895033 创建新的 C# 项目后&#xff0c; Visual Studio 会自动创建一系列的目录与文件。 程序员后面的工…

.net core 6 使用注解自动注入实例,无需构造注入 autowrite4net

像java使用autowrite一样使用 1、前提先注册到ioc容器当中 builder.Services.AddScoped 2、nuget引入AutoWrite4Net 3、启用 //启用自动注入 app.UseAutoWrite(); 4、在类上使用注解 [StartAutoWrite] public class NacosController : ControllerBase 5、实例上使用注解 …

Parade Series - RTSP - Web

Cron Startup Scripts └─ lt-init.cmd├─ lt-server.cmd│ └─ lt-rtsp-proxy-server.cmd│ └─ lt-rtsp-proxy-agent.cmd└─ lt-push.cmd└─ lt-rtsp-cam-daemon.cmd└─ lt-rtsp-cam-worker.cmdlt-init.cmd :: :: PLEASE DO NOT EDIT THIS FILE :: ECHO OFF…

使用Python的pygame库实现下雪的效果

使用Python的pygame库实现下雪的效果 关于Python中pygame游戏模块的安装使用可见 https://blog.csdn.net/cnds123/article/details/119514520 先给出效果图&#xff1a; 源码如下&#xff1a; import pygame import random# 初始化pygame pygame.init()# 设置屏幕尺寸 width…

【办公类-21-03】20240119 提取不连续的男女学号 set()和list法

背景需求&#xff1a;了解班级幼儿性别比例 查看点名册&#xff0c;发现中4班最初的学号是按照先男后女的方式排列&#xff0c;但是随着幼儿转出&#xff0c;空出一些学号&#xff0c;于是新插班的孩子就插入空的学号&#xff0c;空格插完了&#xff0c;就排在学号尾部。 我想…

「优选算法刷题」:盛最多水的容器

一、题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容器…

【AI接口】语音版、文心一言大模型和AI绘图、图片检测API

文章目录 一、语音版大模型AI1、接口2、请求参数3、请求参数示例4、接口返回示例 二、AI图片鉴黄合规检测API1、接口2、请求参数3、请求参数示例4、接口返回示例5、报错说明6、代码开源 三、人工智能AI绘画API1、接口2、请求参数3、请求参数示例4、接口返回示例5、AI绘画成果展…

字符串系列之滑动窗口

大家好&#xff0c;滑动窗口算法一般是作用在字符串或者数组上&#xff0c;通过不断的滑动逻辑窗口&#xff0c;在特定窗口大小内进行计算的过程。滑动窗口的方式可以降低时间复杂度&#xff0c;从而减短计算的执行时间。 比如说在字符串s"5189623196" 中&#xff0…