Studying-代码随想录训练营day40| 198.打家劫舍、213.打家劫舍II、337.打家劫舍III

第40天,动态规划part07,动态规划经典题型“打家劫舍”(ง •_•)ง,编程语言:C++

目录

198.打家劫舍 

213.打家劫舍II 

337.打家劫舍III 

总结 


198.打家劫舍 

文档讲解:代码随想录打家劫舍

视频讲解:手撕打家劫舍

题目: 198. 打家劫舍 - 力扣(LeetCode)

学习:本题根据题意,对于一个房间来说,我们有偷和不偷两种选择。我们在选择每一间房间偷还是不偷的时候,都是为了得到最大的金钱数。我们从动规五部曲出发进行分析。

1.确定dp数组以及下标的含义:我们可以设置一个一维dp数组,dp[i]表示从0-i个房间,我们能够偷到的最大值。

2.确定递推公式:dp[i]能够由谁得来呢?这取决于我们对i房间是偷还是不偷。如果我们偷i房间,则dp[i]应该是dp[i - 2]+nums[i],意味着隔一个房间不偷加上本房间的财富的价值。如果我们不偷i房间,则dp[i]应该是dp[i - 1],也即不偷这个房间,我们能偷的最大价值就应该是上一个房间能够偷的最大价值。最后我们可以得到递推公式为dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])。

3.初始化dp数组:由递推公式我们可知需要初始化至少两个数,dp[0]和dp[1]。显然dp[0]表示0号房间的价值,dp[0] = nums[0]。dp[1]则不能直接赋为nums[1]因为对于dp[1]来说也可以选择取或者不取nums[1],因此dp[1]应该是max(nums[0], nums[1])。

4.确定遍历顺序,从i = 2开始遍历到最后一个节点。

5.举例dp数组

代码:

//时间复杂度O(n)
//空间复杂度O(n)
class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0];
        //1.确定dp数组以及下标的含义, dp[j]表示偷窃0-i个房间能够得到的最高金额
        vector<int> dp(nums.size(), 0);
        //2.确定递推公式:dp[i] = max(dp[i-1],dp[i - 2] + nums[j]);
        //3.初始化dp数组:
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        //4.确定遍历顺序
        for(int i = 2; i < nums.size(); i++) {
            dp[i] = max(dp[i-1], dp[i - 2] + nums[i]);
        }
        return dp[nums.size() - 1];
    }
};

213.打家劫舍II 

文档讲解:代码随想录打家劫舍II

视频讲解:手撕打家劫舍II

题目:213. 打家劫舍 II - 力扣(LeetCode)

学习:本题与上一题不同之处在于,本题是一个环形数组,头部和尾部是连在一起的,这会导致第一个房间和最后一个房间不能同时取用。

但换言之我们可以分开讨论这两种情况。我们可以先考虑不包含尾元素的情况,也就是仅考虑[0, nums.size() - 2]的这个区间,对于这个区间来说,我们要求得能够得到的最大金额,就和上一题是一样的了,采用的方法也相同。

同理我们可以再考虑不包含头元素的情况,也就是仅考虑[1, nums.size() - 1]这个区间,对于这个区间来说,同样得到最大金额的办法和上一题一样。最后我们再取这两个方法得到的最大值之中的更大的那个即可。

接着我们来考虑,这样会不会导致出现遗漏呢,对于第一种不考虑尾元素的情况,假如得到的最大值的取用中取用了头元素,那即使是考虑了尾元素,我们也不能取;同理对于第二种不考虑头元素的情况,假如得到的最大值的取用中取用了尾元素,那即使考虑了头元素,我们也还是不能取;而第三种情况假如头元素和尾元素都没有取用,那就意味着[1, nums.size() - 2]中我们能够得到最大值,即使加上头元素和尾元素,也无法得到更大的值,因为第一种情况和第二种情况,已经把这种可能性包括了。

因此,我们可以分两次得到两种可能性的值,最后得到最大的。

代码:要十分注意起始遍历点和终止遍历点的位置。

//时间复杂度O(n) 2n
//空间复杂度O(n) 
class Solution {
public:
    int robrange(vector<int>&nums, int start, int end) {
        if(start == end) { //nums.size()为2的情况
            return nums[start];
        }
        //1.确定dp数组以及下标含义
        vector<int> dp(nums.size()); 
        //2.确定递推公式:dp[i] = max(dp[i-1],dp[i - 2] + nums[i]);
        //3.初始化dp数组
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        //4.确定遍历顺序
        for(int i = start + 2; i <= end; i++) {
            dp[i] = max(dp[i-1],dp[i - 2] + nums[i]);
        }
        return dp[end];
    }
    int rob(vector<int>& nums) {
        if(nums.size() == 0) return 0;
        if(nums.size() == 1) return nums[0];
        int result1 = robrange(nums, 0, nums.size() - 2); //不考虑尾元素
        int result2 = robrange(nums, 1, nums.size() - 1); //不考虑头元素

        return max(result1, result2);
    }
};

337.打家劫舍III 

文档讲解:代码随想录打家劫舍III

视频讲解:手撕打家劫舍III

题目: 337. 打家劫舍 III - 力扣(LeetCode)

学习: 本题与上两题不同在于,从一个一维数组变成了一颗二叉树,增大了遍历的复杂度,本质还是隔一个搜一个,但需要在动规的过程中加入树的遍历。

我们尝试从动规五部曲进行分析:

1.确定dp数组,我们尝试使用一个map来保存各节点map<Treenode*,int>dp,dp[i].second表示i节点之下能够得到的最大金额。

2.确定递推公式:同样分两种情况,取用当前节点i,dp[i] = i - >val + dp[i->left->left] + dp[i->left->right] + dp[i->right->right] + dp[i->right->left],也就是当前节点的值,加上它的所有孙子的值。如果不取用节点i呢,dp[i] = dp[i->left] + dp[i->right],也就是它的左右孩子的值。

可以发现到这里,在实际使用的时候,我们需要先遍历所有的节点,且确定哪些节点是哪些节点的孙子孩子,这在实际中是很困难的,因此本题的dp数组的设置,与我们常规的dp数组的设置不同,本题的递推公式主要提供了计算的方式。

方法一:暴力搜索

根据递推公式,我们可以采取暴力搜索的方式来得到最大值:

//时间复杂度(n^2)
//空间复杂度O(logn)
class Solution {
public:
    int rob(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right == NULL) return root->val;
        // 偷父节点
        int val1 = root->val;
        if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了
        if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了
        // 不偷父节点
        int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
        return max(val1, val2);
    }
};

方法二:记忆化递推

实际上我们也能够使用上述递推公式的办法,我们可以一边遍历一边存储最大值的方式进行,这样能够在暴力搜索的基础上,降低时间复杂度,减少重复计算:

//时间复杂度O(n)
//空间复杂度O(n)
class Solution {
public:
    unordered_map<TreeNode* , int> umap; // 记录计算过的结果
    int rob(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right == NULL) return root->val;
        if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
        // 偷父节点
        int val1 = root->val;
        if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
        if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
        // 不偷父节点
        int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
        umap[root] = max(val1, val2); // umap记录一下结果
        return max(val1, val2);
    }
};

方法三:动态规划

本题还有个是将动态规划和递归结合的方法,结合递归三部曲和动规五部曲进行。

1.确定递归函数的参数和返回值:

参数显然为各节点,返回值我们设置为dp值,这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

vector<int> robTree(TreeNode* cur) {

换言之我们设置了一个dp数组,dp[0]表示不取用该点时的最大金钱数,dp[1]表示取用该点时的最大金钱数。为啥要设置0,1两种可能,也是为后面的遍历做准备。

2.确定终止条件:遍历到空节点返回

if (cur == NULL) return vector<int>{0, 0};

3.确定遍历顺序:采用后序遍历

vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右

4.确定递推公式:

int val1 = cur->val + left[0] + right[0]; //意味着取用该点时,最大值为该点的值加上左右孩子不用的情况下最大的值。
int val2 = max(left[0], left[1]) + max(right[0], right[1]); //意味着不取用该点时,能够得到的最大值

5.举例推导dp数组:

代码:最后总结得到代码

//时间复杂度O(n)
//空间复杂度O(logn)
class Solution {
public:
    //动态规划+递归
    //1.确定参数及返回值
    vector<int> traversal(TreeNode* cur) {
        //2.确定终止条件
        if(cur == nullptr) return {0,0};

        //3.确定遍历顺序
        vector<int> left = traversal(cur -> left);
        vector<int> right = traversal(cur -> right);
        
        //4.确定递推公式
        int val1 = cur->val + left[0] + right[0]; //意味着取用该点时,最大值为该点的值加上左右孩子不用的情况下最大的值。
        int val2 = max(left[0], left[1]) + max(right[0], right[1]); //意味着不取用该点时,能够得到的最大值
        //返回数值
        return {val2, val1};
    }
    int rob(TreeNode* root) {
        vector<int> dp = traversal(root);
        return max(dp[0], dp[1]);
    }
};

总结 

打家劫舍题型I,II,III,逐一增加难度。第三题也是树形DP的入门题目,通过本题也能够了解树形DP就是在树上进行递归公式的推导。相较于此前我们使用的一维dp数组和二维dp数组还是有很大去别的,需要理解并反复练习。同时我们在第三题中还是用到了记忆化搜索的方式,实际上记忆化搜索也是实现动态规划的方式之一,其与正常的动态规划的方法的区别在于:原文链接

记忆化搜索:「自顶向下」的解决问题,采用自然的递归方式编写过程,在过程中会保存每个子问题的解(通常保存在一个数组或哈希表中)来避免重复计算。

  • 优点:代码清晰易懂,可以有效的处理一些复杂的状态转移方程。有些状态转移方程是非常复杂的,使用记忆化搜索可以将复杂的状态转移方程拆分成多个子问题,通过递归调用来解决。

  • 缺点:可能会因为递归深度过大而导致栈溢出问题。

递推:「自底向上」的解决问题,采用循环的方式编写过程,在过程中通过保存每个子问题的解(通常保存在一个数组或哈希表中)来避免重复计算。

  • 优点:避免了深度过大问题,不存在栈溢出问题。计算顺序比较明确,易于实现。

  • 缺点:无法处理一些复杂的状态转移方程。有些状态转移方程非常复杂,如果使用递推方法来计算,就会导致代码实现变得非常困难。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

人工智能算法工程师(中级)课程13-神经网络的优化与设计之梯度问题及优化与代码详解

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能算法工程师(中级)课程13-神经网络的优化与设计之梯度问题及优化与代码详解。 文章目录 一、引言二、梯度问题1. 梯度爆炸梯度爆炸的概念梯度爆炸的原因梯度爆炸的解决方案 2. 梯度消失梯度消失的概念梯度…

按钮测试: 循环遍历 单据行明细 执行SQL

using Kingdee.BOS.Core.Bill.PlugIn; using Kingdee.BOS.Core.Metadata.EntityElement; using Kingdee.BOS.Orm.DataEntity; using Kingdee.BOS.Util; using System; using System.ComponentModel;using System.Text;namespace cux.button.test {[Description("测试循环遍…

【JavaScript 算法】回溯法:解决组合与排列问题

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、回溯法的基本概念回溯法的模板 二、经典问题及其 JavaScript 实现1. 组合问题2. 排列问题 三、回溯法的应用四、总结 回溯法是一种通过尝试所有可能的解来解决问题的算法策略。它在组合和排列问题中尤为有效&#xff0c…

在 PostgreSQL 里如何处理数据的归档和恢复的权限管理?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 在 PostgreSQL 里如何处理数据的归档和恢复的权限管理&#xff1f;一、数据归档的重要性及方法&#x…

C语言程序设计实例1

程序设计1 问题1_1代码1_1结果1_1 问题1_2代码1_2结果1_2 问题1_3代码1_3结果1_3 问题1_1 计算如下公式&#xff1a; S 3 2 2 − 5 4 2 7 6 2 − ⋅ ⋅ ⋅ − ( − 1 ) n − 1 2 n 1 ( 2 n ) 2 , S \frac{3}{2^2}-\frac{5}{4^2}\frac{7}{6^2}--(-1)^{n-1}\frac{2n1}{(2n)^…

Mac 如何安装vscode

Mac 电脑/ 苹果电脑如何安装 vscode 下载安装包 百度搜索vscode&#xff0c;即可得到vscode的官方下载地址&#xff1a;https://code.visualstudio.com/ 访问网页&#xff0c;点击下载即可。 下载完成后&#xff0c;得到下图所示的app。 将该 app 文件&#xff0c;放入到…

【答疑篇】企业管理咨询公司的服务流程是怎样的?

在竞争激烈的商业环境中&#xff0c;企业如何保持持续的增长和竞争力&#xff1f;答案就隐藏在企业管理咨询公司的服务之中。今天&#xff0c;就让我们一起揭晓企业管理咨询公司的服务流程&#xff0c;看看这些专业团队是如何助力企业焕发新生的&#xff01; 一、需求分析 企业…

yolov5 上手

0 介绍 YOLO(You Only Look Once)是一种流行的物体检测和图像分割模型&#xff0c;由华盛顿大学的约瑟夫-雷德蒙&#xff08;Joseph Redmon&#xff09;和阿里-法哈迪&#xff08;Ali Farhadi&#xff09;开发。YOLO 于 2015 年推出&#xff0c;因其高速度和高精确度而迅速受到…

防火墙双机热备带宽管理综合实验

拓扑图和要求如下&#xff1a; 之前的步骤可以去到上次的实验 1.步骤一&#xff1a; 首先在FW3防火墙上配置接口IP地址&#xff0c;划分区域 创建心跳线&#xff1a; 下面进行双机热备配置&#xff1a; 步骤二&#xff1a; 先将心跳线连接起来 注意&#xff1a;一定要将心跳…

勒索防御第一关 亚信安全AE防毒墙全面升级 勒索检出率提升150%

亚信安全信舷AE高性能防毒墙完成能力升级&#xff0c;全面完善勒索边界“全生命周期”防御体系&#xff0c;筑造边界勒索防御第一关&#xff01; 勒索之殇&#xff0c;银狐当先 当前勒索病毒卷携着AI技术&#xff0c;融合“数字化”的运营模式&#xff0c;形成了肆虐全球的网…

深度解析:如何优雅地删除GitHub仓库中的特定commit历史

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

【Blockly图形化积木编程二次开发学习笔记】1.工具箱的实现

文章目录 Blockly 版本选择上手 Blockly 版本选择 在【兰州大学】Blockly创意趣味编程【全36讲】主讲教师&#xff1a;崔向平 周庆国中提到&#xff0c;在18年6月份之前的版本中&#xff0c;可以通过安装依赖库的方式&#xff0c;打开开发者工具的离线版本&#xff0c;但是新版…

Linux桌面环境手动编译安装librime、librime-lua以及ibus-rime,提升中文输入法体验

Linux上的输入法有很多&#xff0c;大体都使用了Fcitx或者iBus作为输入法的引擎。相当于有了一个很不错的“地基”&#xff0c;你可以在这个“地基”上盖上自己的“小别墅”。而rime输入法&#xff0c;就是一个“毛坯别墅”&#xff0c;你可以在rime的基础上&#xff0c;再装修…

Docker之在外执行docker内部命令(十一)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+AOSP…

什么是CAN总线?

目录 1 CAN总线简介 2 CAN总线物理结构 2.1 CAN总线原理 2.2 CAN总线和I2C 3 CAN的电气属性 4 CAN帧的种类 4.1 CAN帧的种类 4.2 数据帧 4.3 遥控帧 1 CAN总线简介 1.1 CAN是什么&#xff1f; CAN总线&#xff0c;全称为Controller Area Network&#xff0c;即控制器…

【Ubuntu】安装使用pyenv - Python版本管理

当我们在Ubuntu上使用Python进行开发的时候&#xff0c;可能会遇到版本不兼容的问题&#xff0c;当然你可以选择使用apt的方式安装不同版本的python环境 但是存在一定的问题&#xff1a;安装不同版本的Python通常不会改变默认的python3命令指向的版本&#xff0c;而且就算你进行…

《云原生安全攻防》-- 容器攻击案例:Docker容器逃逸

当攻击者获得一个容器环境的shell权限时&#xff0c;攻击者往往会尝试进行容器逃逸&#xff0c;利用容器环境中的错误配置或是漏洞问题&#xff0c;从容器成功逃逸到宿主机&#xff0c;从而获取到更高的访问权限。 在本节课程中&#xff0c;我们将详细介绍一些常见的容器逃逸方…

知识付费小程序源码 thinkphp后台 带3000多条教程数据

知识付费小程序源码 thinkphp后台 带3000多条教程数据,云码素材有进行了更新开发,更新了广告位管理,后台一键更新数据,用户登录 不单单是一个源码,我们对接了云码素材的教程资源,也就是说你可以免费拥有云码素材所有教程资源,后台一键更新,无须自己再更新资源,每天有我们更新,…

【嵌入式开发 Linux 常用命令系列 4.4 -- repo 工具安装】

请阅读【嵌入式开发学习必备专栏 】 文章目录 Repo 工具下载Repo 安装Multiple Git Repository Tool Repo 工具下载 Repo 官网链接 Repo 安装 方法一&#xff1a; # Debian/Ubuntu. $ sudo apt-get install repo# Gentoo. $ sudo emerge dev-vcs/repo方法二&#xff1a; $ …

系统架构设计师 - 系统配置与性能评价

系统配置与性能评价 系统配置与性能评价&#xff08;0 - 2分&#xff09;性能指标 ★ ★硬件软件性能调整 阿姆达尔解决方案 ★性能评价方法 ★ ★ ★ 大家好呀&#xff01;我是小笙&#xff0c;本章我主要分享系统架构设计师 - 系统配置与性能评价知识&#xff0c;希望内容对你…