递归学习——记忆化搜索

2023-09-14 11:41:22

目录

​编辑

一,概念和效果

二,题目

1.斐波那契数

1.题目

2.题目接口

3.解题思路

2.不同的路径

1.题目

2.题目接口

3.解题思路

3.最长增长子序列

1.题目

2.题目接口

3.解题思路

4.猜数字游戏II

1.题目

2.题目接口

3.解题思路

总结:


一,概念和效果

记忆化搜索可以说是带备忘录的递归实现这个算法的目的便是减少递归时对同一个节点的多次遍历从而提高学习效率。学习这个算法,理解这个算法最好的方式便是通过能够用记忆化搜索的题目来理解。

二,题目

1.斐波那契数

1.题目

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

2.题目接口
class Solution {
public:
    int fib(int n) {

    }
};
3.解题思路

相信大家都知道如何解决斐波那契数的问题。这个问题的经典解决方式便是运用到递归解法。使用递归时要明确的便是递归的出口。斐波那契数的递归出口便是在n == 0或者n==1时。这两个条件下的返回值便是它们本身。当n不等于0和1时f(n) = f(n-1)+f(n-2)条件成立。

根据以上思路便可以写出如下解题代码:

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

但是我们都知道在递归时会有大量的重复计算。比如当n == 5时递归展开图如下:

在这里可以看到2这个节点被求了很多次,这样子便是很大的浪费了。这个时候为了提高效率便可以采用记忆化搜索的方式。记忆化搜索的实现其实也非常的简单,也就是当我们得到一个结果时便将其记录下来。当我们想要再次遍历相同的节点时只要看前面是否有记录过,若记录过便不再访问直接返回之前记录过的结果就行了。比如斐波那契数列这道题的记忆化搜索方式的解题代码如下:

class Solution {
public:
    vector<int>memo;//表示备忘录
    int fib(int n) {
        memo = vector<int>(n+1);//初始化
        return dfs(n);
    }

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

        if(memo[n]!=0)//册中已求便无需再求
        {
            return memo[n];
        }
        


        memo[n] = dfs(n-1)+dfs(n-2);//记录在册
       return memo[n];
    
    }
};

2.不同的路径

1.题目

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

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

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

2.题目接口
class Solution {
public:
    int uniquePaths(int m, int n) {

    }
};
3.解题思路

因为机器人只能向前或者向下走,所以要到达finish位置的话首先就要到达finish的上面和左边这两个位置:

要到达这两个位置的话便又要到达这两个位置的上面和下面位置。以次类推得到公式:

f(finishi,finishj) = f(finishi-1,finishj)+f(finishi,finishj-1)。得到这个关系我们便可以知道这道题和斐波那契数列有的一拼,所以自然就会想到递归的解法。在这里便要寻找递归条件了。

1.因为目中给的m与n表示的是网格的长与宽。所以当m == 1&&n == 1时就意味着网格里面只有一个格子,也就是机器人就在右下角的格子了所以返回1。

2.当给的m与n中有一个为0的话,也就是网格的长或者宽为0,也就表示没有格子于是返回0。

根据以上分析写出代码如下:

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

    int dfs(int m,int n)
    {
        if(m == 1&&n==1)
        {
            return 1;
        }

        if(m ==0||n==0)
        {
            return 0;
        }

         return dfs(m-1,n)+dfs(m,n-1);

    }
};

但是这个代码会超时,所以我们得优化这个代码让这个代码变得更快。优化的方式便是记忆化搜索:

class Solution {
public:
    vector<vector<int>>memo;

    int uniquePaths(int m, int n) {
       memo = vector<vector<int>>(m+1,vector<int>(n+1));
       return dfs(m,n);
    } 

    int dfs(int m,int n)
    {
        if(memo[m][n]!=0)
        {
            return memo[m][n];
        }

        if(m == 1&&n==1)
        {
            return 1;
        }

        if(m ==0||n==0)
        {
            return 0;
        }
         
         memo[m][n] = dfs(m-1,n)+dfs(m,n-1);
         return  memo[m][n];

    }
};

可以看到这道题的记忆化搜索处理方式和上一道题一毛一样。

3.最长增长子序列

1.题目

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

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

2.题目接口
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {

    }
};
3.解题思路

这道题的解题思路其实也不太难,就是要遍历一下每一个下标然后求出以每一个下标的起点为开始位置的子序列长度。但是因为我们要求的是最大长度所以需要比较更新最大长度。以这个思路写成代码如下:

class Solution {
public:
     int len; 
    int lengthOfLIS(vector<int>& nums) {
       len = nums.size();
       int ret = 0;
       for(int i = 0;i<len;i++)
       {
           ret = max(ret,dfs(i,nums));//得到以某个下标为起点的最大长度
       } 
       return ret;
    }

    int dfs(int i,vector<int>&nums)
    {
        int ret = 1;//每一个子序列的最小长度为1
        for(int x =i+1;x<len;x++ )
        {
            if(nums[x]>nums[i])
            {
               ret = max(ret,dfs(x,nums)+1);//求出以每一个下标为起点的子序列长度,并每次都更新一下
            }
        }
        return ret;//返回最大值
    }
};

但是以上代码是通不过的,因为时间限制:

那该怎么办呢?其实很简单,还是和前两道题一样要采用一个记忆化搜索的策略:

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

    int dfs(int i,vector<int>&nums)
    {
        if(memo[i]!=1)//判断一下
        {
            return memo[i];
        }
        int ret = 1;
        for(int x =i+1;x<len;x++ )
        {
            if(nums[x]>nums[i])
            {
               ret = max(ret,dfs(x,nums)+1);
            }
        }

        memo[i] = ret;//记录一下
        return ret;
    }
};

然后便过掉了:

4.猜数字游戏II

1.题目

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

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

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

2.题目接口
class Solution {
public:
    int getMoneyAmount(int n) {

    }
};
3.解题思路

这道题该咋做呢?或者说这道题是什么意思呢?以输入一个数字10为例吧。我们要猜数字时便要在[1,10]之间猜测。于是我们猜数字策略便有很多种。比如一下几种:

1.当开始位置为1时

这里的至少要1+2+3+4+5+6+7+8块钱。

2.当我们一开始便选到5时:

还有一种便是这道题目给的最优解法:

在这个最优解法里边我们要做的便是找到这里的最大钱数也就是7+9 = 16。所以我们要做的便暴力搜索找出这个最优策略里的最大钱数。怎么做呢?其实还是遍历,抽象成下图:

这里一个一个试验的i便是为了得到最优模型而设计的。返回最大值便是为了得到每一个模型里面的最坏情况然后让每一个情况比较一下得到最优模型的最坏情况。写成代码如下:

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

    int dfs(int left,int right)
    {
        if(left>=right)//当数组范围中只有一个数字或者范围不合法时便返回0。
        {
            return 0;
        }
         
         int ret = INT_MAX;//记录最优模型的最坏情况。
        for(int head = left;head<right;head++)
        {
            int l = dfs(left,head-1)+head;//左结果
            int r = dfs(head+1,right)+head;//右结果
            ret = min(ret,max(l,r));//最优模型下的追怀情况

        }

        return ret;
    }
};

这样便得到了代码了,这个代码是对的但是遗憾的是这个代码过不了:

接下来采用记忆化搜索方式:

class Solution {
public:
    vector<vector<int>>memo;
    int getMoneyAmount(int n) {
        memo = vector<vector<int>>(n+1,vector<int>(n+1));
       return dfs(1,n);
    }

    int dfs(int left,int right)
    {
        if(memo[left][right]!=0)
        {
            return memo[left][right];
        }
        if(left>=right)
        {
            return 0;
        }
         
         int ret = INT_MAX;
        for(int head = left;head<right;head++)
        {
            int l = dfs(left,head-1)+head;
            int r = dfs(head+1,right)+head;
            ret = min(ret,max(l,r));

        }
       
        memo[left][right] = ret;
        return ret;
    }
};

这样便可以过掉了:

总结:

其实记忆化搜索的目的便是实现剪枝操作,提高递归效率。当我们的递归操作里有大量的重复的递归操作时便可以用记忆化搜索的方式来提高递归效率。

更多推荐

安卓机型-MTK芯片掉串码 掉基带 如何用工具进行修复 改写参数

在早期MTK芯片机型中较多使用APBP方式来修复mtk芯片机型的串码。目前MTK机型对于丢基带掉串码问题大都使用MODEMMETA工具来进行修复串码或者改写参数。今天以一款mtk芯片机型来做个演示,高通芯片类的可以参考;高通改串相关工具仅支持在联发科芯片组上运行的设备,无论是智能手机还是平板电脑。它不适用于联发科以外的

vue模板语法(上集)

为什么要用Vue模板语法Vue模板语法是Vue.js框架的一部分,使用它有以下几个优点:简化了HTML代码编写:Vue模板语法支持简化HTML标签和属性的写法,使得代码更加简洁易读,提高开发效率。数据绑定:Vue模板语法支持数据双向绑定,可以将数据自动更新到对应的DOM元素,从而避免了手动操作DOM的麻烦。条件渲染:V

为Electron-log 设置日志颜色

使用Electron-log为你的Electron应用添加日志颜色在Electron应用中,有效的日志记录是一项不可或缺的任务,它可以帮助你跟踪应用程序的运行状况、调试问题以及监视用户体验。为了提高日志的可读性,你可以使用Electron-log以及Node.js中的Chalk模块来为不同的日志级别添加颜色和样式。安装

浅谈建筑能耗智能监测平台发展现状及未来趋势

安科瑞华楠摘要:文章以每年发布的上海市国家机关办公建筑和大型公共建筑能耗监测及分析报告变化为切入点,分析了历年能耗分析报告的内容和功能变化;介绍了上海市国家机关办公建筑和大型公共建筑能耗监测平台发展和应用历程;揭示了当下显现的问题,并以问题为导向,预测了未来发展的趋势。关键词:国家机关办公建筑和大型公共建筑;能耗监测;

洁净室/净化车间:洁净等级划分及标准、检测方法及流程解读

无尘车间的发展与现代工业、尖端技术紧密的联系在一起。目前在生物制药、医疗卫生、食品日化、电子光学、能源、精密器械等行业运用已经相当的普遍且成熟。空气洁净度等级(aircleanlinessclass):洁净空间单位体积空气中,以大于或等于被考虑粒径的粒子最大浓度限值进行划分的等级标准。国内按空态、静态、动态对无尘车间进

MySQL详细案例 1:MySQL主从复制与读写分离

文章目录1.MySQL主从复制1.1使用场景1.2MySQL的复制类型1.3主从复制的作用1.4主从复制的工作过程1.5实现MySQL主从复制1.5.1前置准备1.5.2主服务器mysql配置1.5.3从服务器1mysql配置1.5.4从服务器2mysql配置1.5.5测试1.6主从复制的3种同步模式1.6.1异步复制

实时数仓混沌演练实践

一、背景介绍目前实时数仓提供的投放实时指标优先级别越来越重要,不再是单独的报表展示等功能,特别是提供给下游规则引擎的相关数据,直接对投放运营的广告投放产生直接影响,数据延迟或者异常均可能产生直接或者间接的资产损失。从投放管理平台的链路全景图来看,实时数仓是不可或缺的一环,可以快速处理海量数据,并迅速分析出有效信息,同时

Java JVM分析利器JProfiler 结合IDEA使用详细教程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、JProfiler是什么?二、我的环境三、安装步骤1.Idea安装JProfiler插件1.下载程序的安装包四、启动前言对于我们Java程序员而言,肯定需要对项目工程进行JVM监控分析,最终选择jprofiler,它可以远程链接,使用方便,

opencv图像像素类型转换与归一化

文章目录opencv图像像素类型转换与归一化1、为什么对图像像素类型转换与归一化2、在OpenCV中,`convertTo()`和`normalize()`是两个常用的图像处理函数,用于图像像素类型转换和归一化;(1)`convertTo()`函数用于将一个`cv::Mat`对象的像素类型转换为另一种类型。它的基本用法

第8章 MySQL的数据目录

8.1数据库和文件系统的关系像InnoDB、MyISAM这样的存储引擎都是把表存储在磁盘上的,而操作系统用来管理磁盘的又被称为文件系统,所以用专业一点的话来表述就是:像InnoDB、MyISAM这样的存储引擎都是把表存储在文件系统上的。当我们想读取数据的时候,这些存储引擎会从文件系统中把数据读出来返回给我们,当我们想写

公私钥非对称加密 生成和验证JSON Web Token (JWT)

前言这是我在这个网站整理的笔记,关注我,接下来还会持续更新。作者:神的孩子都在歌唱公私钥非对称加密生成和验证JSONWebToken什么是JSONWebToken(JWT)Java程序中生成和验证JWT代码解析什么是JSONWebToken(JWT)JSONWebToken(JWT)是一种轻量级的身份验证和授权机制,由

热文推荐