代码随想录算法训练营DAY47|C++动态规划Part8|198.打家劫舍、213.打家劫舍II、198.打家劫舍III

news2024/11/18 9:49:02

文章目录

  • 198.打家劫舍
    • 思路
    • CPP代码
  • ⭐️213.打家劫舍II
    • 解决环的问题
    • 思路
    • 总结
    • CPP代码
  • ⭐️198.打家劫舍III
    • 思路
      • 递归三部曲——确定参数和返回值
      • 递归三部曲——确定终止条件
      • 递归三部曲——确定单层遍历的逻辑
    • 打印dp数组
    • CPP代码
    • 暴力递归
    • 记忆化递归

198.打家劫舍

力扣题目链接

文章讲解:198.打家劫舍

视频讲解:动态规划,偷不偷这个房间呢?| LeetCode:198.打家劫舍
状态:由于我们偷当前房屋,肯定会影响后续的偷窃活动,所以本题肯定是动态规划。总体来说本题还不算难,但是非常重要!

这里把题目再抽象一下:

偷钱币的规则——相邻房间不能偷,然后随便怎么偷都行

本题最大的难点就是,虽说相邻房间不能偷,但是剩下的都可以偷,甚至隔好几个房间再偷。那么当前这个房间到底偷不偷呢?这是最关键的点

其实我们当前房间偷不偷本质上依赖于我们前一个房间和前两个房间偷与不偷的状态,如果前一个房间被偷了,那本房间绝对不能偷,如果偷了前两个房间,那么这个房间可以偷也可以不偷,所以当前房间偷不偷和前一个和前两个房间的状态是由关系的,这个关系也就是我们的递推关系。

思路

  • dp数组的含义

我们考虑数组下标i(包含下标i),它所偷的最大的钱币就是dp[i],最终的结果在dp[nums.size() - 1]

  • 递推公式

对于房间i,无非就是偷或者不偷,

如果我们明确要偷i,那么i前面的那个房间是一定不考虑的,所以最多只能考虑到dp[i-2],其中的dp[i-2]表示在i-2(包含)及之前的所有房间所能偷的最大金币数量再加上i房间的金币数量,所以递推公式

d p [ i − 2 ] + n u m s [ i ] dp[i-2]+nums[i] dp[i2]+nums[i]

如果我们明确不偷i,此时我们就可以考虑i-1以及i-1之前的所有房间所能偷的最大金币数量,所以递推公式

d p [ i − 1 ] dp[i-1] dp[i1]

综上所示,递推公式为

dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
  • 初始化

从递归公式可以看出来dp[i]是由dp[i-2]dp[i-1]推导出来的,所以递推公式的基础就是dp[0]dp[1]

很明显dp[0]=nums[0] ,至于dp[1]=max(nums[0], nums[1])

非零和非一下标初始化成0即可

  • 遍历顺序

    这里遍历顺序就很简单了,没什么讲究

for (int i = 2; i < nums.size(); i++)
  • 打印

CPP代码

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        vector<int> dp(nums.size());
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size(); i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[nums.size() - 1];
    }
};

⭐️213.打家劫舍II

力扣题目链接

文章讲解:213.打家劫舍II

视频讲解:动态规划,房间连成环了那还偷不偷呢?| LeetCode:213.打家劫舍II
状态:这里成环的处理逻辑还是很牛逼的,并且非常通用,需要重点解决

本题与198.打家劫舍有什么区别呢?唯一的区看别就是本题把线性数组首尾相连连成环了,这会导致规则有什么样的变化呢?

  • 相邻房间不偷,这是肯定的
  • 首尾元素只能选一个偷。因为本题首尾相连了!

一旦连成环就很容易懵,本题还有起点和终点吗?起点和终点应该如何选呢?

解决环的问题

如果连成环的话,首尾元素如果要选的话只能选择一个,要不选首元素要不尾元素,要不都不选。其实本质上我们可以划分成三个情况,这里拿[1, 6, 1, 9, 1]举例

  • 情况一:不考虑首元素也不考虑尾元素,只考虑中间的[6, 1, 9]数组范围

此时,该数组变成了一个线性数组,所以我们其实可以用线性数组的方式来解决本题,一下就变成了198.打家劫舍

  • 情况二:考虑首元素,不考虑尾元素了,即考虑[1, 6, 1, 9]数组范围。所以本题仍然能变成一个线性数组
  • 情况三:考虑尾元素,不考虑首元素了,即考虑[6, 1, 9, 1]数组范围。所以仍然变成一个线性数组解体

本题中的考虑首位元素并不代表我一定会选择首元素或尾元素,具体的选择是由递推公式决定的

现在再来分析一下这三种情况,从元素情况其实就可以看出,其实情况二和情况三已经包含了情况一的中间元素了。

所以综上所述:我们只要求情况二和情况三的最优解,然后取一个最大值就行

思路

上文中其实已经阐明了大部分的思路,现在我们的情况二和情况三已经是线性数组了,我们只需要把情况二[1, 6, 1, 9]传到我们的198.打家劫舍函数中,然后把情况三传到198.打家劫舍函数,取一个max

总结

我们竟然把一个环形数组的题解耦成了一个线性数组!卡哥牛逼!

CPP代码

// 注意注释中的情况二情况三,以及把198.打家劫舍的代码抽离出来了
class Solution {
public:
    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);
    }
    // 198.打家劫舍的逻辑
    int robRange(vector<int>& nums, int start, int end) {
        if (end == start) return nums[start];
        vector<int> dp(nums.size());
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[end];
    }
};

⭐️198.打家劫舍III

力扣题目链接

文章讲解:198.打家劫舍III

视频讲解:动态规划,房间连成树了,偷不偷呢?| LeetCode:337.打家劫舍3

状态:入门级树形dp,需要在二叉树中进行一个状态转移。只能说卡哥太牛逼了,这题的讲解真的是绝

基本规则和198.打家劫舍一样,只不过我们本题中数据结构变成了一个二叉树。

本题如果对二叉树不够了解,对二叉树的遍历不够娴熟的话,基本就是寄。

所以本题的思路也应该是以递归三部曲为主旋律,穿插动态规划五部曲的过程

思路

本文以root=[3, 4, 5, 1, 3, 5, 1]为例,

本题既然是遍历树形结构,那么肯定逃不开递归,所以本质是在单层递归逻辑中采用了动态规划的相关思想。

递归三部曲——确定参数和返回值

参数可能还懵懵懂懂能猜到,肯定是传入根结点!

那返回值是什么呢?dp数组是什么呢?如何融入树形遍历中呢?

首先最主要的问题肯定是解决dp数组的含义,在树形结构的递归遍历中,有一个最基本的思想,也就是不同的遍历方法可以去处理不同的逻辑。本题中,如果我们想偷当前节点,那么相邻结点也就是左右孩子节点肯定不能偷;如果偷左右孩子结点,那么当前节点就不能偷。所以肯定是用后序遍历,因为我们的当前结点的状态只要根据左右孩子的状态来的。

总而言之,这种情况最好的方法就是,用dp数组去定义每个结点的状态。

  • 确定dp数组的含义

每个节点只有两个状态,也就是说节点偷还是不偷,所以用一个长度为2的一维dp数组**dp[0]表示不偷当前节点情况的最大价值,dp[1]表示偷**当前结点情况的最大价值。总而言之就是每一个结点,都存在这样一个dp数组状态

  • dp数组的递推公式

本题使用的是后序遍历的方式,由底向上推,最后就有根结点是偷还是不偷的两个状态,那我们最终的结果就是根结点偷还是不偷的两个状态我们取一个最大值,那这个就是我们最后的结果。

那么我们定义当前节点左右孩子的dp数组状态为leftdprightdp

再回到当前节点偷与不偷,当然选一个dp数组的最大值,所要要定义一个value1表示偷当前结点我们取得的最大钱币,那么如果我们偷了当前结点,那么本节点的左右孩子就一定不能偷了,那么我们要计算不偷左孩子能得到的最大钱币加上不偷右孩子能得到的最大钱币;

int value1 = cur->val + leftdp[0] + rightdp[0]

好了,现在我们必须要考虑偷左右孩子的情况了,如果偷左右任意一个孩子的话,那我们就不能偷当前结点。 再一个,左孩子偷不偷不是由我们去决定的,而是由leftdp[0]leftdp[1]谁大就选哪个;右孩子偷还是不偷也是一样的道理,并且左右孩子是可以同时偷的,因为他们不算相邻结点。所以以代码如下:

int value2 = max(leftdp[0], leftdp[1]) + max(rightdp[0], rightdp[1]);

此时,我得到了两个状态value1value2,value1是我偷当前结点的结果,value2是我不偷当前结点的结果,那么当前状态的偷和不偷就出来了,所以我们返回什么呢?

// 返回当前节点的dp数组
return {value2, value1} //{不偷,偷}

递归三部曲——确定终止条件

if (cur == NULL) return vector<int> (0, 0); //本质上是返回空间的dp数组的状态

如果当前节点是NULL的话,也就是我们此时遍历到了空节点,我们此时偷与不偷其实都是0,所以要返回一个元素都是0的dp数组。

递归三部曲——确定单层遍历的逻辑

我们已经在递归三部曲——确定参数和返回值:确定dp数组的递推公式中讲完了。

打印dp数组

以下图为例:

各个节点的dp数组为:(两列分别为dp[0]、dp[1],行数为后序遍历的顺序)

CPP代码

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]);
    }
    //长度为2的数组, 0:不偷 1:偷
    vector<int> robTree (TreeNode* cur) {
        if (cur == NULL) return vector<int> {0, 0};
        vector<int> leftDp = robTree(cur->left);
        vector<int> rightDp = robTree(cur->right);

        //偷cur,那么就不能偷左右结点。
        int val0 = cur->val + leftDp[0] + rightDp[0];
        //不偷cur,那么可以偷也可以不偷左右结点,则取较大的情况
        int val1 = max(leftDp[0], leftDp[1]) + max(rightDp[0], rightDp[1]);
        return {val1, val0};
    }
};

暴力递归

这里介绍一下暴力递归的办法,我觉得有利于加深树形dp的理解

int rob(TreeNode* root) {
    if(!root) return 0; //无结点
    if (!root->right && !root->left) return root->val;   //只有根结点

    //偷父节点
    int val1 = root->val;
    if (root->left) val1 += rob(root->left->left) + rot(root->left->right);
    if (root->left) val1 += rob(root->right->left) + rot(root->right->right);

    //不偷父节点
    int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子

    return max(val1, val2);

时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( l o g n ) O(logn) O(logn)

记忆化递归

在暴力解法中,由于每个节点被访问多次(每次父节点考虑偷或不偷时),整体时间复杂度是指数级的。

所以我们可以用一个map把计算过的结果保存一下,那么计算过孙子节点,那么计算孩子节点的时候可以复用结果。
最核心的代码:

if (umap[root]) return umap[root];
...
umap[root] = max(val1, val2);
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);
    }
};

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

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

相关文章

安卓开发--按键跳转页面,按键按下变色

前面已经介绍了一个空白按键工程的建立以及响应方式&#xff0c;可以参考这里&#xff1a;安卓开发–新建工程&#xff0c;新建虚拟手机&#xff0c;按键事件响应。 安卓开发是页面跳转是基础&#xff01;&#xff01;&#xff01;所以本篇博客介绍利用按键实现页面跳转&#…

【C++】STL-list的使用

目录 1、list的使用 1.1 list的构造 1.2 list的遍历 1.3 list capacity 1.4 list element access 1.5 容量相关 list是一个带头双向循环链表 1、list的使用 1.1 list的构造 1.2 list的遍历 list只有两种遍历方式&#xff0c;因为没有operator[] 因为list的双向链表&am…

初识C语言——第十六天

C语言中的语句结构类型:顺序/选择/循环 分支语句 if else switch 循环语句 while for do whlie goto语句 代码练习:找两个整数的最大公约数和最小公倍数 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h>//int main() //{ // int age 60; // if (ag…

景源畅信数字:抖音小店的入住门槛大不大?

近年来&#xff0c;随着短视频平台的崛起&#xff0c;抖音小店逐渐成为了众多商家和创业者关注的焦点。那么&#xff0c;抖音小店的入住门槛究竟大不大呢?本文将从四个方面对这一问题进行详细阐述。 一、注册流程 抖音小店的注册流程相对简单&#xff0c;只需按照官方指引完成…

漫威争锋Marvel Rivals申请测试资格教程 最新获取测试资格教程

即将震撼登场的《漫威争锋》&#xff08;Marvel Rivals&#xff09;标志着PvP射击游戏领域的全新突破&#xff0c;由漫威官方网站全力支持推出。这款游戏定于5月11日早晨9点启幕其封闭Alpha测试章节&#xff0c;这场测试盛宴将持续整整十天。在首波测试浪潮中&#xff0c;玩家有…

以目录创建的conda环境添加到jupyter的kernel中

场景&#xff1a;由于某些原因&#xff0c;服务器上的conda环境不能通过--name的方式创建&#xff0c;只能通过指定目录即-p的方式&#xff0c;在这种情况下该环境在conda env list中没有显示&#xff0c;无法在jupyter kernel中搜到&#xff0c;只能手动添加。 1.进入环境 # …

百面算法工程师 | 模型评价指标及优化策略

本文给大家带来的百面算法工程师是深度学习模型评价指标的面试总结&#xff0c;文章内总结了常见的提问问题&#xff0c;旨在为广大学子模拟出更贴合实际的面试问答场景。在这篇文章中&#xff0c;我们还将介绍一些常见的评价方案&#xff0c;并提供参考的回答及其理论基础&…

扩散模型~

推荐&#xff1a;write_own_pipeline.ipynb - Colab (google.com) 基本管道 一直显示NVIDIA有问题&#xff0c;所以就把.to("cuda")去掉了&#xff0c;使用Colab运行的&#xff0c;代码如下&#xff1a; from diffusers import DDPMPipelineddpm DDPMPipeline.fr…

【有趣的透镜】1.透镜初相识

1.透镜的外形和材料 (1)透镜由玻璃或者塑料制成&#xff1b; (2)透镜一般为圆型&#xff0c;其单面或双面为球面&#xff1b; 2.透镜的类型和折射 (1)球面外凸为凸透镜(聚光)&#xff0c;球面内凹为凹透镜(散光)&#xff1b; (2)透镜是基于光的折射&#xff0c;只要光从一…

【api接口开通教程】YouTube Data API v3申请流程

一、背景调查 1.1 API接口介绍 采集youtube数据&#xff0c;大体分为两种方案&#xff1a;一种是基于爬虫&#xff0c;一种是基于API接口。 说人话就是&#xff1a;爬虫相当于走后门、爬窗户&#xff08;利用技术手段窃取&#xff0c;人家没说给&#xff0c;但我硬拿&#x…

兔子繁衍问题<C语言>

问题&#xff1a;一对兔子&#xff0c;从出生后第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又生一对兔子。假如兔子都不死&#xff0c;请问第1个月出生的一对兔子&#xff0c;至少需要繁衍到第几个月时兔子总数才可以达到N对&#xff1f; 分析&#xff1a; 可以观…

学习记录:AUTOSAR R20-11的阅读记录(三)【CP(5.1-5.5)】

接上回&#xff1a;学习记录&#xff1a;AUTOSAR R20-11的阅读记录&#xff08;二&#xff09;【AP】 五、CP 在AUTOSAR_TR_ClassicPlatformReleaseOverview.pdf里面对CP文档进行分类&#xff1a; Release DocumentationCommunicationMemorySystem ServicesMCALIOLibrariesDi…

这 7 道 Redis 基础问题,很常见!!

后端项目如果用到分布式缓存的话&#xff0c;一般用的都是 Redis。不过&#xff0c;Redis 不仅仅能做缓存&#xff0c;还能用作分布式锁、延时队列、限流等等。 什么是 Redis&#xff1f; Redis[1] &#xff08;REmote DIctionary Server&#xff09;是一个基于 C 语言开发的…

数据治理的难题:如何化解?

在数字化转型的大潮中&#xff0c;数据治理成了每个企业都绕不开的话题。但是&#xff0c;数据治理这条路并不好走&#xff0c;充满了各种挑战。这些挑战不仅来自于技术&#xff0c;还有组织文化、流程和法律法规等方面。 挑战一&#xff1a;数据孤岛 在企业内部&#xff0c;…

ORA-02020:过多的数据库链接在使用

一、问题描述 今天同事说&#xff0c;有一个查询功能&#xff0c;同时查了几个子平台的dblink&#xff0c;页面返回报错。 提示ORA-02020&#xff1a;过多的数据库链接在使用&#xff1b; bad SQL grammar 二、解决办法 1&#xff09;分析业务需求 分析业务场景 &#xff0c;发…

【勘误】一个错误的快速排序实现

文章目录 问题一&#xff1a;不一致算法描述部分给出的分划实现完整程序部分给出的分划实现 问题二&#xff1a;不正确问题三&#xff1a;把循环条件改为 i < j 程序还是不正确正确的实现总结 从 10 10 10 年前我开始学 C 语言时我就认为快速排序并不是个简单的算法。相比于…

哈迪斯2发售时间 哈迪斯游戏攻略 苹果电脑怎么玩《哈迪斯2》

这两年肉鸽游戏大爆发&#xff0c;只要不是美女抽卡养成那基本上就是肉鸽了&#xff0c;但是真正让玩家口服心服的肉鸽游戏不多&#xff0c;《哈迪斯》绝对算是其中一款。 近日让玩家期待已久的肉鸽大作&#xff0c;晶体管工作室制作的《哈迪斯》正统续作《哈迪斯2》终于开卖了…

3.ERC4626

ERC4626是一个vault&#xff0c;在DAI中&#xff0c;使用ETH换取DAI。其流程为先充值ETH到maker vault。 Vault 资产的管理、分红用户充值某项资产获取某个凭证该凭证作为分红、推出的依据Yield Farming/借贷/质押等 以太坊改进提案EIP:ethereum improvemwnt proposal 最初E…

大数据面试题 —— 数据库

目录 关系型数据库与非关系型数据库的区别数据库三范式MySQL中 drop、delete、truncate的区别MySQL中 char和 varchar 的区别MySQL中inner join、left join、right join以及full join的区别MySQL中 having 和 where 的区别count(*)、count(1)、count(列名)的区别MySQL中视图和表…

ardupilot的固定翼飞行模式

飞行模式 APM所有的飞行模式都在对应的机型的文件夹下的mode.h里面有定义,针对于不同的模型,功能函数在基类中Mode中都是以纯虚函数实现了, 然后在继承的子类中重新实现它,以实现多态。 takeoff模式 参见网址在 ArduPlane 4.0 及更高版本中,自动起飞本身也是一种模式(…