代码随想录Day 39|打家劫舍问题,leetcode题目:198.打家劫舍、213.打家劫舍Ⅱ、337.打家劫舍Ⅲ

news2024/9/17 9:04:13

提示:DDU,供自己复习使用。欢迎大家前来讨论~

文章目录

  • 题目
    • 题目一:198.打家劫舍
      • 解题思路:
    • 题目二:213.打家劫舍II
      • 解题思路:
    • 题目三: 337.打家劫舍 III
      • 解题思路
        • 暴力递归
        • 记忆化递推
        • 动态规划
  • 总结

题目

题目一:198.打家劫舍

[198. 打家劫舍](https://leetcode.cn/problems/combinations/)

解题思路:

初次接触这类问题时,可能会对如何决策感到困惑:是选择偷窃当前房屋还是跳过。

但深入思考后,是否偷窃当前房屋实际上取决于前一个房屋和前两个房屋的偷窃情况。

**这种决策与之前状态之间的依赖关系,**正是动态规划递推公式的核心所在

通过建立这种依赖关系,我们可以逐步构建出最优解。

打家劫舍问题就是动态规划解决的经典例子,它展示了如何通过分析状态之间的相互影响来找到最优解。

动规五部曲分析如下:

  1. 确定dp数组(dp table)以及下标的含义

dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

  1. 确定递推公式

决定dp[i]的因素就是第i房间偷还是不偷。

  • 偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,相邻房间不能偷,所以是i - 1 - 1= i - 2

  • 不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房)。

  • 最后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

  1. dp数组如何初始化

从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值(相邻只能选择一个偷)即:dp[1] = max(nums[0], nums[1]);

代码如下:

vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
  1. 确定遍历顺序

dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!(要利用到前面的数组

代码如下:

for (int i = 2; i < nums.size(); i++) {
    dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
  1. 举例推导dp数组

以示例二,输入[2,7,9,3,1]为例。

198.打家劫舍

红框dp[nums.size() - 1]为结果。

以上分析完毕,C++代码如下:

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];
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

小结:打家劫舍是DP解决的经典题目,这道题也是打家劫舍入门级题目

题目二:213.打家劫舍II

213. 打家劫舍 II

解题思路:

这道题目和198.打家劫舍 (opens new window)是差不多的,唯一区别就是成环了。

将题目进行分解:

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素

213.打家劫舍II

  • 情况二:考虑包含首元素,不包含尾元素

213.打家劫舍II1

  • 情况三:考虑包含尾元素,不包含首元素

213.打家劫舍II2

在处理数组问题时,虽然需要考虑包含数组尾部元素的情况(情况三),但并不是说必须选择尾部元素。

选择的关键在于最大化结果,而不是盲目包含特定元素。 对于情况三,取nums[1] 和 nums[3]就是最大的。

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了

分析到这里,本题其实比较简单了。 剩下的和198.打家劫舍 (opens new window)就是一样的了。

代码如下:

// 注意注释中的情况二情况三,以及把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];
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

题目三: 337.打家劫舍 III

[337. 打家劫舍 III](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/)

解题思路

当前讨论的树结构问题与“打家劫舍”系列问题(如LeetCode的198和213题)在本质上是类似的,都是关于选择和决策的问题。

树的遍历:对于树的问题,首先需要确定遍历方式。可以选择深度优先搜索(前序、中序、后序遍历)或广度优先搜索(层序遍历)。

  1. 遍历顺序的选择:本题需要使用后序遍历,这是因为后序遍历先处理子节点,再处理当前节点,符合题目中通过递归函数返回值来计算下一步的需求。

  2. 决策点:与“打家劫舍”问题一样,本题的关键在于决定是否“抢劫”(或选择)当前节点。如果选择了当前节点,那么它的子节点就不能被选择;如果没有选择当前节点,那么可以考虑选择其子节点。

总结:理解树的遍历方式和如何根据子节点的状态来决定当前节点的决策是解决这类问题的关键。后序遍历提供了一种自然的解决方案,因为它允许我们先解决子问题,再根据子问题的解来解决当前问题。

暴力递归

代码如下:

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^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
  • 空间复杂度:O(log n),算上递推系统栈的空间

当然以上代码超时了,这个递归的过程中其实是有重复计算了。

计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。

记忆化递推

所以可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。

代码如下:

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);
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(log n),算上递推系统栈的空间
动态规划

在上面两种方法,其实对一个节点 偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。

而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解

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

一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

参数为当前节点,代码如下:

vector<int> robTree(TreeNode* cur) {

其实这里的返回数组就是dp数组。

所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

所以本题dp数组就是一个长度为2的数组!

在递归过程中,系统会自动为每一层递归保存其参数,因此即使使用长度为2的数组来记录每个节点的偷与不偷状态,也能通过递归调用和系统栈的自然保存机制来正确处理树中每个节点的状态。

  1. 确定终止条件

在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

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

这也相当于dp数组的初始化

  1. 确定遍历顺序

首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。

通过递归左节点,得到左节点偷与不偷的金钱。

通过递归右节点,得到右节点偷与不偷的金钱。

代码如下:

// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
  1. 确定单层递归的逻辑
  • 如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];

  • 如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

代码如下:

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

// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
  1. 举例推导dp数组

以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导

img

最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱

递归三部曲与动规五部曲分析完毕,C++代码如下:

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> left = robTree(cur->left);
        vector<int> right = robTree(cur->right);
        // 偷cur,那么就不能偷左右节点。
        int val1 = cur->val + left[0] + right[0];
        // 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
        int val2 = max(left[0], left[1]) + max(right[0], right[1]);
        return {val2, val1};
    }
};
  • 时间复杂度:O(n),每个节点只遍历了一次
  • 空间复杂度:O(log n),算上递推系统栈的空间

小结:

树形动态规划并不神秘,它只是将动态规划和贪心算法的概念扩展到了树结构上。理解树的遍历方式是解决这类问题的关键。

  1. 树形DP的本质
    • 树形动态规划是动态规划在树结构上的应用,核心在于递归地在树上推导状态转移公式。
  2. 树形DP的遍历方式
    • 与传统的在数组上进行动态规划不同,树形DP需要对树进行遍历,这通常涉及到深度优先搜索(DFS)或广度优先搜索(BFS)。

总结

打家劫舍问题是一个经典的动态规划问题,通常用来描述在一系列房屋中进行选择以最大化收益的场景,同时遵守一定的约束(如不能连续偷窃相邻的房屋)。

  1. 状态定义
    • 定义 dp[i] 为考虑到第 i 个房屋时,能够偷窃到的最大金额。
  2. 状态转移方程
    • 状态转移考虑两种情况:偷窃当前房屋(dp[i] = dp[i-1] + nums[i],其中 nums[i] 是第 i 个房屋的金额)和不偷窃当前房屋(dp[i] = dp[i-1])。
    • 因此,dp[i] = max(dp[i-1], dp[i-2] + nums[i]),这表示可以选择偷窃当前房屋(此时不能偷窃前一个房屋)或者不偷窃当前房屋。
  3. 边界条件
    • 初始化 dp[0] 为 0,表示没有房屋时的金额为 0。
    • dp[1] 为第一个房屋的金额,表示只有第一个房屋可以偷窃时的最大金额。

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

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

相关文章

Linux基础2-权限2(操作权限,粘滞位,umask,目录文件的rwx权限)

上篇内容&#xff1a;Linux基础2-权限1(用户&#xff0c;权限是什么&#xff1f;)-CSDN博客 目录 一. 权限的操作&#xff08;命令&#xff09; 1.1 chmod 1.2 chown 1.3 chgrp 二. 粘滞位 三. umask&#xff08;遮掩码&#xff09; 四. 目录文件的 r w x 权限 一. 权限…

数据库的操作:SQL语言的介绍

一.前言 SQL是一种结构化查询语言。关系型数据库中进行操作的标准语言。 二.特点 ①对大小写不敏感 例如&#xff1a;select与Select是一样的 ②结尾要使用分号 没有分号认为还没结束; 三.分类 ①DDL&#xff1a;数据定义语言&#xff08;数据库对象的操作&#xff08;结…

服务器重装系统,数据备份 容器备份

文章目录 1.前言2.docker备份2.1 容器备份2.2 镜像备份2.3 数据卷备份 3.docker安装4.jdk安装5.导入镜像6.导入容器 本文档只是为了留档方便以后工作运维&#xff0c;或者给同事分享文档内容比较简陋命令也不是特别全&#xff0c;不适合小白观看&#xff0c;如有不懂可以私信&a…

【最新华为OD机试E卷-支持在线评测】计算疫情扩散时间(200分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-E/D卷的三语言AC题解 💻 ACM金牌🏅️团队| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,…

DDComponentForAndroid:探索Android组件化方案

在现代Android应用开发中&#xff0c;随着应用规模的不断扩大&#xff0c;传统的单体应用架构已经无法满足快速迭代和维护的需求。组件化架构作为一种解决方案&#xff0c;可以将应用拆分成多个独立的模块&#xff0c;每个模块负责特定的功能&#xff0c;从而提高代码的可维护性…

2.ChatGPT的发展历程:从GPT-1到GPT-4(2/10)

引言 在人工智能领域&#xff0c;自然语言处理&#xff08;NLP&#xff09;是连接人类与机器的重要桥梁。随着技术的不断进步&#xff0c;我们见证了从简单的文本分析到复杂的语言理解的转变。ChatGPT&#xff0c;作为自然语言处理领域的一个里程碑&#xff0c;其发展历程不仅…

【C/C++】C++程序设计基础(继承与派生、多态性)

目录 八、继承与派生8.1 派生类的引入与特性8.2 单继承8.3 同名成员的访问方式8.4 赋值兼容规则8.5 单继承的构造与析构8.6 多继承 九、多态性9.1 运算符重载9.2 虚函数9.3 纯虚函数与抽象类 八、继承与派生 8.1 派生类的引入与特性 -继承:一旦指定了某种事物父代的本质特征&a…

线程相关内容

线程 一、介绍二、thread库1、构造函数&#xff08;1&#xff09;函数&#xff08;2&#xff09;说明&#xff08;3&#xff09;注意 2、join函数3、detach4、joinable函数5、get_id函数 三、mutex的种类1、mutex&#xff08;1&#xff09;介绍&#xff08;2&#xff09;lock&a…

vant UI之van-tab如何实现标题两行显示

前言&#xff1a; 相必大家在开发移动端或者小程序时都会见到如下设计稿 这个时候大家基本上都会想到使用vant UI 的van-tab组件&#xff0c;如果实现不了那就自己封装一个tab组件这样的情况。 其实使用van-tab是可以实现的&#xff0c;不过要借助van-tab的一系列api和css&…

数据结构(2):LinkedList和链表[1]

下面我们来介绍一种新的数据结构&#xff0c;链表。 我们曾经讨论过顺序表。它的数据存储在物理和逻辑上都是有逻辑的。而我们今天要学习的链表&#xff0c;则在物理结构上非连续存储&#xff0c;逻辑上连续。 1.链表的认识 链表由一个一个的节点组成。 我们可以想象一列火…

乐鑫安全制造全流程

主要参考资料&#xff1a; 【乐鑫全球开发者大会】DevCon24 #10 &#xff5c;乐鑫安全制造全流程 乐鑫官方文档Flash加密: https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/security/flash-encryption.html 【ESP32S3】使用 Flash 下载工具完成 Flash 加密功能…

C++ | Leetcode C++题解之第394题字符串解码

题目&#xff1a; 题解&#xff1a; class Solution { public:string src; size_t ptr;int getDigits() {int ret 0;while (ptr < src.size() && isdigit(src[ptr])) {ret ret * 10 src[ptr] - 0;}return ret;}string getString() {if (ptr src.size() || src[…

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目&#xff1a; 题解&#xff1a; static const int MASK1 1 << 7; static const int MASK2 (1 << 7) (1 << 6);bool isValid(int num) {return (num & MASK2) MASK1; }int getBytes(int num) {if ((num & MASK1) 0) {return 1;}int n 0;in…

windows电脑自动倒计时关机

今天聊一聊其他的。我时不时的有一个需求&#xff0c;是关于在windows电脑上定时关机。 不知道怎么地&#xff0c;我好几次都忘了这个自动定时关机的终端命令&#xff0c;于是每一次都要去网上查。 1.鼠标右击【开始菜单】选择【运行】或在键盘上按【 WinR】快捷键打开运行窗口…

【变化检测】基于STANet建筑物(LEVIR-CD)变化检测实战及ONNX推理

主要内容如下&#xff1a; 1、LEVIR-CD数据集介绍及下载 2、运行环境安装 3、STANet模型训练与预测 4、Onnx运行及可视化 运行环境&#xff1a;Python3.8&#xff0c;torch1.12.0cu113 likyoo变化检测源码&#xff1a;https://github.com/likyoo/open-cd 使用情况&#xff1a…

力扣周赛:第414场周赛

&#x1f468;‍&#x1f393;作者简介&#xff1a;爱好技术和算法的研究生 &#x1f30c;上期文章&#xff1a;[首期文章] &#x1f4da;订阅专栏&#xff1a;力扣周赛 希望文章对你们有所帮助 本科打ACM所以用的都是C&#xff0c;未来走的是Java&#xff0c;所以现在敲算法还…

探索未来住宿新体验:酒店智能开关引领的智慧生活

酒店智能开关作为智慧酒店的重要组成部分&#xff0c;正悄然改变着我们的旅行住宿方式&#xff0c;让每一次入住都成为一场科技与舒适的完美邂逅。 智能开关&#xff1a;重新定义酒店房间的每一个角落 传统酒店中&#xff0c;房间的灯光、空调、窗帘等设备的控制往往依赖于手动…

LCD字符图片显示——FPGA学习笔记11

一、字模显示原理 字模数据&#xff1a;将这个0/1矩阵按照屏幕扫描的顺序以字节的形式体现。 取模软件设计&#xff1a; 点阵数要按照实际情况填写 二、实验任务 本节的实验任务是通过开发板上的RGB TFT-LCD接口&#xff0c;在RGB LCD液晶屏的左上角位置从上到下依次显示图片以…

【数据结构】希尔排序(缩小增量排序)

目录 一、基本思想 1.1 引入希尔排序的原因 1.2 基本思想 二、思路分析 三、gap分组问题 四、代码实现 4.1 代码一&#xff08;升序&#xff09; 4.2 代码二&#xff08;升序&#xff09; 五、易错提醒 六、时间复杂度分析 七、排序小tips 一、基本思想 1.1 引入希尔…

Vue3:<Teleport>传送门组件的使用和注意事项

你好&#xff0c;我是沐爸&#xff0c;欢迎点赞、收藏、评论和关注。 Vue3 引入了一个新的内置组件 <Teleport>&#xff0c;它允许你将子组件树渲染到 DOM 中的另一个位置&#xff0c;而不是在父组件的模板中直接渲染。这对于需要跳出当前组件的 DOM 层级结构进行渲染的…