【递归、回溯专题(三)】记忆化搜索题集

news2024/11/10 18:43:17

文章目录

  • 1. 斐波那契数
  • 2. 不同路径
  • 2. 不同路径
  • 3. 最长递增子序列
  • 4. 猜数字大小II

1. 斐波那契数

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

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

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

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

    0 <= n <= 30

解法一:递归
时间复杂度是O(N2)
以n=5为例对应的递归展开图如下,
在这里插入图片描述

class Solution {
public:
    int fib(int n) {
        return dfs(n);
    }
    int dfs(int n)
    {
        if(n <= 1) return n;
        return dfs(n-1) + dfs(n-2);
    }
};

解法二:记忆化搜索
记忆化搜索:在递归过程中遇到一些完全相同的问题,将完全相同的问题记录在备忘录中,等到下一次遇到了相同的问题直接去备忘录取值,无需再进行深度优先遍历。记忆化搜索又称为带备忘录的递归。
在这里插入图片描述实现记忆化搜索的步骤:

  1. 创建一个备忘录
  2. 递归每次返回之前,先将结果添加到备忘录中
  3. 在每次进入递归的时候,查找备忘录中是否存在即将要递归的值

时间复杂度:O(N)

class Solution {
public:
    int memory[31]; // 备忘录
    int fib(int n) {
        // 初始化备忘录的值为-1, -1的含义是指当前的斐波那契数未被计算
        memset(memory, -1, sizeof memory);
        return dfs(n);
    }
    int dfs(int n)
    {
        // 进入递归之前要先查找备忘录
        if(memory[n] != -1) // 不等于-1代表之前处理过相同的问题
            return memory[n]; // 剪枝

        // 返回之前要先添加在备忘录
        if(n <= 1)
        {
            memory[n] = n;
            return n;
        }
        // 返回之前要先添加在备忘录
        memory[n] = dfs(n-1) + dfs(n-2);
        return memory[n];
    }
};

解法三:动态规划
细节问题详见动态规划章节。
时间复杂度:O(N)

class Solution {
public:
    int fib(int n) {
        // 创建dp表
        int dp[31] = {0};
        // 初始化dp表
        dp[0] = 0, dp[1] = 1;
        for(int i = 2; i <= n; i++)
        {
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
};

问题讨论

  1. 记忆化搜索和动态规划的关系,在《算法导论》这本书中一起被归纳为动态规划。它们都是暴力搜索,都是将已经计算好了的值存起来;只不过记忆化搜索是递归的形式,而常规的动态规划是递推的形式。
  2. 并不是所有的递归(深搜、暴搜),都可以改写成记忆化搜索,因为记忆化搜索处理的是递归当中大量的重复问题。

2. 不同路径

一个机器人位于一个 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

提示:

    1 <= m, n <= 100
    题目数据保证答案小于等于 2 * 109

记忆化搜索解法:

2. 不同路径

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

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

问总共有多少条不同的路径?
在这里插入图片描述
算法原理:

  1. 备忘录表的设计
    大小为m+1,n+1. 因为题目要到达下标(m,n),所以标的大小要设计为m+1,n+1。
  2. 函数体的设计
    在主函数中调用dfs(m,n),其含义是传入我要到达(m,n)时,有多少路径。
    在递归函数dfs中,递归结束标志是:
if(i == 0 || j == 0) return 0;// 含义是到达下标0位置有多少种方法,由于是非法位置(不会到达的位置),所以设置为0
if(i == 1 && j == 1) return 1;// 到达下标(1,1)位置有多少种方法,因为是起点,所以就是一种方法

递归函数返回值,和动态规划一样,返回值是上边和左边的种类之和

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

在这里插入图片描述

  1. 能否记忆化搜索?
    记忆化搜索就是在返回之前要将结果记录在备忘录。
    答案是能,因为递归会出现重复相同的操作
    在这里插入图片描述

暴力解法:
暴力解法会超时

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);
}
int uniquePaths(int m, int n) {
    return dfs(m, n);
}

记忆化搜索:
记忆化搜索是由暴力解法改写来的。

int dfs(vector<vector<int>> &memory, int i, int j)
{
    // 递归出口 
    if(i == 0 || j == 0) return 0;
    if(i == 1 && j == 1) {
        memory[1][1] = 1;
        return 1;
    }
    // 查找备忘录中是否已经填入了, 判断是否需要递归下去
    if(memory[i][j] != -1) return memory[i][j]; // 剪枝
    // 返回之前记录返回值在备忘录中
    memory[i][j] = dfs(memory, i-1, j) + dfs(memory, i, j-1);
    return memory[i][j];
}
int uniquePaths(int m, int n) {
    vector<vector<int>> memory(m + 1, vector<int>(n + 1, -1));
    return dfs(memory, m, n);
}

动态规划版本:
dp数组的含义:到达下标(i, j)一共有多少种方法。

int uniquePaths(int m, int n) {
    vector<vector<int>> dp(m + 1, vector<int>(n + 1)); // 加1加的是虚拟节点
    // 初始化dp数组
    dp[0][1] = 1;
    // 遍历dp数组
    for(int i = 1; i <= m; i++)
        for(int j = 1; j <= n; j++)
            dp[i][j] = dp[i][j-1] + dp[i-1][j];
    return dp[m][n];
}

3. 最长递增子序列

算法原理:
以一个数为子序列的起始位置,去它的后面找:
在这里插入图片描述
根据上图编写代码:
但是会超出时间限制,原因就是因为出现了很多重复的递归操作。

int lengthOfLIS(vector<int>& nums) 
{
    int ret = 0;
    for(int i = 0; i < nums.size(); i++)
    {
        // 以第i个元素开头的递增子序列
        ret = max(ret, dfs(nums, i)); // 取以每个元素为开头递归返回来的递增子序列最大长度
    }
    return ret;
}
int dfs(vector<int>& nums, int pos)
{
    int ret = 1; // 这里ret要初始化为1, 因为如果ret没有被更新, 那么要返回当前节点的数目1
    // 在pos的后面选一个数构成子序列
    for(int i = pos + 1; i < nums.size(); i++)
    {
        if(nums[i] > nums[pos])
        {
            ret = max(ret, dfs(nums, i) + 1); // +1代表将该节点本身加上去
        }
    }
    return ret;
}

记忆化搜索:
在这里插入图片描述

int memo[2501] = {0}; // 备忘录, 存放以i位置为开头的递增子序列长度
int lengthOfLIS(vector<int>& nums) 
{
    int n = nums.size();
    int ret = 0;
    for(int i = 0; i < n; i++)
    {
        // 以第i个元素开头的递增子序列
        ret = max(ret, dfs(nums, i));
    }
    return ret;
}
int dfs(vector<int>& nums, int pos)
{
    if(memo[pos] != 0) return memo[pos];

    int ret = 1; // 这里ret要初始化为1, 因为如果ret没有被更新, 那么要返回当前节点的数目1
    // 在pos的后面选一个数构成子序列
    for(int i = pos + 1; i < nums.size(); i++)
    {
        if(nums[i] > nums[pos])
        {
            ret = max(ret, dfs(nums, i) + 1); // +1代表将该节点本身加上去
        }
    }
    memo[pos] = ret;
    return ret;
}

动态规划版本:

  • dp数组的含义:dp[i]表示以i位置为起点的最长递增子序列的长度
  • 填表顺序:从后往前
    在之前的递归中递归是遍历到了只有一个元素或者元素中不构成升序就返回,也就是说返回是从后往前的。在动态规划当中,我们应从dp表的最后一个元素开始填,从后往前填。
int lengthOfLIS(vector<int>& nums) 
{
    int n = nums.size();
    vector<int> dp(n, 1); // 代表一个元素构成子序列时长度为1
    
    int ret = 0; // 找dp表的最大值
    // 填写dp表
    for(int i = n - 1; i >= 0; i--) // 找以i位置为开头的最长子序列长度
    {
        for(int j = i + 1; j < n; j++) // 从i的后面寻找子序列
        {
            if(nums[i] < nums[j])
            {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        ret = max(ret, dp[i]);
    }
    return ret;
}

4. 猜数字大小II

在这里插入图片描述在这里插入图片描述在这里插入图片描述

算法原理:
在这里插入图片描述

暴力枚举所有情况:
但是会超时。

int dfs(int left, int right)
{
    // 遇到叶子结点即为获胜
    if(left >= right) return 0;
    int ret = INT_MAX; // 用于存储获胜的最小现金数
    // 从区间[left,right]选择头节点
    for(int h = left; h <= right; h++)
    {
        // lower
        int l = dfs(left, h - 1);
        // higher
        int r = dfs(h + 1, right);
        // 为确保获胜的最大现金数为
        int chmp = max(l, r) + h;
        // 已经确保了区间[left,right]为头节点的子树组成的情况获胜, 但目前需要最少的钱
        ret = min(ret, chmp);
    }
    return ret;
}
int getMoneyAmount(int n) {
    return dfs(1, n);
}

记忆化搜索能否实现?答案是能
在这里插入图片描述记忆化搜索:

class Solution {
public:
    int memo[201][201];
    int dfs(int left, int right)
    {
        // 遇到叶子结点即为获胜
        if(left >= right) return 0;

        // 判断区间[left,right]是否需要递归下去
        if(memo[left][right] != 0) return memo[left][right];

        int ret = INT_MAX; // 用于存储获胜的最小现金数
        // 从区间[left,right]选择头节点
        for(int h = left; h <= right; h++)
        {
            // lower
            int l = dfs(left, h - 1);
            // higher
            int r = dfs(h + 1, right);
            // 为确保获胜的最大现金数为
            int chmp = max(l, r) + h;
            // 已经确保了区间[left,right]为头节点的子树组成的情况获胜, 但目前需要最少的钱
            ret = min(ret, chmp);
        }

        // 记录区间的返回值
        memo[left][right] = ret;
        return ret;
    }
    int getMoneyAmount(int n) {
        return dfs(1, n);
    }
};

暴力搜索:会超时
注意一下,这题可以不加vis数组,因为vis数组主要是处理重复元素问题的,但我这里找的是递增序列,往回找时不会再次重复进入dfs。
在这里插入图片描述

class Solution {
public:
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1};
    int m, n;
    int dfs(vector<vector<int>>& matrix, int i, int j)
    {
        // 记录4个方向的最长路径
        int ret = 1;
        // 4个方向的dfs
        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            int count = 0; // 记录一个方向能走多远
            if(x >= 0 && x < m && y >= 0 && y < n
                && matrix[i][j] < matrix[x][y])
            {
                count = dfs(matrix, x, y) + 1;
            }
            ret = max(ret, count);
        }
        // 走到了尽头, count不会被更新
        return ret;
    }
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        m = matrix.size(), n = matrix[0].size();
        int 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;
    }
};

记忆化搜索:
尝试用一下记忆化搜索。首先我们看一下如果以1为起点进行dfs,和以6为起点进行dfs是不是有重复的dfs动作。那么以6为起点的路径就直接返回得到结果就行,无需再进行dfs了。
在这里插入图片描述

class Solution {
public:
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1};
    int memo[201][201];
    int m, n;
    int dfs(vector<vector<int>>& matrix, int i, int j)
    {
        // 以(i,j)这个位置为起点的路径之前已经搜索过了
        if(memo[i][j] != -1) return memo[i][j];

        // 记录4个方向的最长路径
        int ret = 1;
        // 4个方向的dfs
        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            int count = 0; // 记录一个方向能走多远
            if(x >= 0 && x < m && y >= 0 && y < n
                && matrix[i][j] < matrix[x][y])
            {
                count = dfs(matrix, x, y) + 1;
            }
            ret = max(ret, count);
        }

        // 走到了尽头, count不会被更新
        memo[i][j] = ret; // 记录在备忘录
        return ret;
    }
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        m = matrix.size(), n = matrix[0].size();
        memset(memo, -1, sizeof(memo)); // 初始化备忘录
        int 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;
    }
};

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

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

相关文章

逻辑器件输出高阻态时,输出端口的电平是什么状态呢?

高阻态是逻辑器件输出端口的一种状态&#xff0c;当端口处于高阻态时&#xff0c;输入端口的电平变化不会引起输出端口变化&#xff0c;不会对与之相连的后级输入端口或总线产生影响&#xff0c;对于总线架构的电路极为重要。   输出端口处于高阻态时&#xff0c;输出端口处于…

什么是分段和分页?

内存管理的必要性 很早之前计算机只能运行单个进程&#xff0c;就算运行批处理程序&#xff0c;也是棑好对&#xff0c;一个一个的进行处理&#xff0c;不存在多个进程并发运行&#xff0c;这时候内核对于内存管理相对比较简单&#xff0c;直接把物理内存地址拿过来是使用即可。…

网络编程TCP和UDP

将TCP的CS模型再敲一遍 TCP服务器 1->创建原始的套接字描述符 2->将原始套接字与主机ip绑定 3->将原始套接字设置监听状态 4->接收客户端连接&#xff0c;获取客户端信息&#xff0c;因为原始套接字被用了&#xff0c;所以创建新的套接字描述符用于客户端通信…

2区SCI仅52天直录!9月甄选SCI/SSCI合集(含各领域)

【SciencePub学术】我处SciencePub学术2024年9月SCI/SSCI/EI/CNKI刊源表已更新&#xff01;内含多本中科院TOP、CCF推荐以及进展超顺的优质期刊&#xff0c;最快1-2个月录用&#xff01; 计算机领域重点SCI 环境地质化学领域重点SCI 生物医学领域重点SCI 数学材料物理领域重点S…

拥有这些AI绘画网站,让你轻松告别手绘时代!

在这个充满无限可能的数字世界里&#xff0c;AI 绘画动漫网站已经成为了许多艺术家和设计师的新宠。从手绘时代的岁月如歌&#xff0c;到今天科技的飞速发展&#xff0c;我们已经可以用AI技术创作出令人惊叹的艺术作品&#xff0c;打开了全新的创作空间。接下来&#xff0c;就让…

图形化编程011

项目描述&#xff1a; 点击绿旗&#xff0c;点击空格键&#xff0c;角色向上游动&#xff0c;松开空格键&#xff0c;角色向下飘落。 浮游生物碰到角色会发出声音并隐藏&#xff0c;碰到舞台边缘会重新出现。 30秒后程序结束 。 拆解步骤&#xff1a; 1、添加背景和角色以及…

如何打造一个智能化的远程在线考试系统?

远程教育与在线考试已成为提升知识传播效率和学习灵活性的重要手段。 土著刷题在线考试系统&#xff0c;凭借其完善的多功能考试模块&#xff0c;为教育机构、学校乃至企业提供了一个智能化的远程在线考试解决方案。 接下来将介绍土著刷题在线考试系统如何助力用户构建一个高效…

第L3周:机器学习-逻辑回归

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目标&#xff1a; 逻辑回归适用于分类问题&#xff0c;主要用于解决二分类或多分类的问题。比如&#xff1a;用户购买某商品的可能性&#xff0c;某病人患有某…

单端输入与差分输入

单端输入&#xff1a; 单端输入测量的是一根导线与地之间的电压差&#xff0c;然后将这个差值放大以提供输出。 然而&#xff0c;单端输入容易受到噪声的影响&#xff0c;因为承载信号的导线会吸收电背景噪声。此外&#xff0c;单端输入上的信号还可能受到地环路的影响。 例…

鸿蒙(API 12 Beta6版)图形【NativeDisplaySoloist开发指导】方舟2D图形服务

如果开发者想在独立线程中进行帧率控制的Native侧业务&#xff0c;可以通过DisplaySoloist来实现&#xff0c;如游戏、自绘制UI框架对接等场景。 开发者可以选择多个DisplaySoloist实例共享一个线程&#xff0c;也可以选择每个DisplaySoloist实例独占一个线程。 接口说明 函…

【MySQL00】【 杂七杂八】

文章目录 一、前言二、MySQL 文件1. 参数文件2. 日志文件3. 套接字文件4. pid 文件5. 表结构定义文件6. InnoDB 存储引擎文件 二、BTree 索引排序三、InnoDB 关键特性1. 插入缓冲1.1 Insert Buffer 和 Change Buffer1.1 缓冲合并 2. 两次写2. 自适应哈希索引3. 异步IO4. 刷新邻…

宝藏!《联盟现代控制特训班题库》(麒麟篇) 讲义部分:甄选内容

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;初试《现代控制特性班题库》(麒麟篇)&#xff0c;分为讲义和习题两个部分&#xff0c;本文为讲义部分的甄选内容&#xff0c;一本书帮你学透现控。 目录 Part1&#xff1a;资料封面&目录 讲义部分目录 Part2&…

为什么很多人都还用着Win10 而不投向Win11的怀抱?

前言 自从Windows 11发布&#xff08;2021年6月&#xff09;不知不觉已经过去了3年了。在这三年期间&#xff0c;Windows 11经历了几个大版本的更新。 Windows 10从正式发布开始&#xff08;2015年7月&#xff09;至现在已经有9年的历史。而Windows 10的最后一个版本22H2即将在…

2024年河南省成人高考报名指南

2024年河南省成人高考报名指南 河南省成人高考预计9月初g网报名&#xff0c;一般一周时间结束&#xff0c;预计到9月中旬报名截止‼不清楚报名流程同学看过来&#xff0c;今天老师给大家详细介绍一下&#xff01; 想参加河南成人高考需要什么条件&#xff0c;具体的报名流程是什…

AD19基础应用技巧:捕捉对象功能的讲解鼠标”绿色十字”大光标、小光标切换

AD PCB 中心点捕捉功能&#xff1a; 线段、圆、边框中心点捕捉。 有时候不想要鼠标自动捕捉中心点怎么办&#xff1f; 关于Altium Designer 20 的捕抓功能的讲解&#xff08;https://blog.csdn.net/weixin_44599693/article/details/126177841&#xff09; ——- AD PCB画板…

基于VsCode和Git的代码版本管理

基础回退 在项目文件夹右键打开git bash&#xff0c;输入命令git log 查看提交的历史 commit&#xff0c;git log --prettyoneline将版本信息压缩到一行 使用git log可能会显示不全&#xff0c;按enter逐行查看&#xff0c;按end跳至末尾查看完成后&#xff0c;按q即可退出 …

湖南省工业废水高氯酸盐排放新标准解析与技术应对策略

随着工业化进程的加快&#xff0c;环境污染问题变得尤为突出。特别是在某些特定行业产生的污染物&#xff0c;如高氯酸盐&#xff0c;因其对环境和人体健康的潜在危害&#xff0c;越来越受到关注。为了解决这一问题&#xff0c;湖南省生态环境厅与省市场监督管理局联合制定了《…

AN7536PT时钟电路

目录 1 时钟电路概述2 时钟晶振电路2.1 需求分析2.2 晶振选型&#xff08;Datasheet表5-7解读&#xff09;2.3 设计晶振电路&#xff08;表4-1、图5-4&#xff09; 1 时钟电路概述 时钟电路是一种用于产生稳定、周期性脉冲信号的电子电路。它通常由晶体振荡器和相关逻辑电路组…

Kettle 锁表原因及解决办法【源码级分析】

文章目录 背景源码分析锁表场景1:资源库锁表锁表场景2:写日志锁表在哪里配置的kettle_log_table?官方解释自增 SQL 获取 BatchI 原理解决自增 SQL 获取 BatchID背景 Kettle 7.1.0 经常出现锁表的情况,体现为在数据库里有一条锁表 SQL,然后整个 Kettle 都无法运行。😂�…

java简介以及单个或两个及以上java安装与环境变量配置

目录 1.java简介 1.1 Java 的主要特点包括 1.2 Java 的主要用途包括&#xff1a; 1.3 java下载地址 2.安装java 2.1 安装程序 2.2选择安装路径 2.3等待安装 3.首先&#xff0c;进入环境变量 3.1 找到设置&#xff08;第一个win11&#xff0c;第二个win10&#xff09;…