当前位置: 首页 > news >正文

24暑假算法刷题 | Day39 | 动态规划 VII | LeetCode 198. 打家劫舍,213. 打家劫舍 II,337. 打家劫舍 III

目录

  • 198. 打家劫舍
    • 题目描述
    • 题解
  • 213. 打家劫舍 II
    • 题目描述
    • 题解
  • 337. 打家劫舍 III
    • 题目描述
    • 题解


打家劫舍的一天 😈

198. 打家劫舍

点此跳转题目链接

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

题解

动态规划解决。根据题意,每次要考虑的无非是“要不要抢当前这家”,如果要抢,则肯定不能抢前一家;否则,要考虑抢前一家。

⚠️ 注意这里只是 考虑 抢前一家,不是一定要抢,因为可能出现间隔两家抢到的总金额更高的情况。

  • dp 数组含义: dp[i] 表示抢前 i 家,能抢到的最高金额

  • 状态转移方程: dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

    如果抢当前这家,即抢一个 nums[i] ,则不能抢前一家,所以“衔接”的是再前面一家的情况,即 dp[i - 2] ——注意这里仍不表示一定会抢第 i - 2 家,只是利用之前已求得的抢前 i - 2 家能获得的最高金额。

    如果不抢当前这家,则直接用前一家的结果,即 dp[i - 1]

    上述两种取较大的结果,即 dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

代码(C++)

int rob(vector<int>& nums) {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

点此跳转题目链接

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

题解

这题和 198. 打家劫舍 差不多,只不过数组变成了循环结构。所以最大的变化就是“首尾相连”了,而根据题目,每次要考虑的仍然是“相邻两家”不能同时抢,即数组的一头一尾不能一起抢,其他的其实和原版一致。

因此,既然首尾不能同时出现,我们不妨直接分别考虑 “有头无尾”“无头有尾” 两种情况,再取其中较大的结果即可。

代码(C++)

class Solution
{
private:// 普通的打家劫舍int robRange(const vector<int> &nums, int start, int end){if (end == start)return nums[start];vector<int> dp(end - start + 1);dp[0] = nums[start];dp[1] = max(nums[start], nums[start + 1]);for (int i = 2; i < dp.size(); ++i)dp[i] = max(dp[i - 2] + nums[start + i], dp[i - 1]);return dp[end - start];}public:int rob(vector<int> &nums){if (nums.size() == 1)return nums[0];return max(robRange(nums, 0, nums.size() - 2), robRange(nums, 1, nums.size() - 1));}
};

337. 打家劫舍 III

点此跳转题目链接

题目描述

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

示例 1:

在这里插入图片描述

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

在这里插入图片描述

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104

题解

由于二叉树的结构,不难想到从根节点往下递归搜索的方法,每次考虑抢当前节点、不抢孩子节点和不抢当前节点、可能抢孩子节点的情况即可。同时,用一个哈希表存储已经求过的节点的最大金额,实现记忆化,降低时间开销:

class Solution1I // 记忆化的递归暴力搜索
{
private:unordered_map<TreeNode*, int> valueMap; // 记忆化:记录搜索过的节点public:int rob(TreeNode *root){if (!root)return 0; // 空节点,没法抢if (!root->left && !root->right)return root->val; // 没有左右孩子,直接抢当前节点// 有记忆:直接采用求过的该节点能偷的最大金额if (valueMap.find(root) != valueMap.end())return valueMap[root];// 偷当前节点(父节点),则不能偷左右孩子,但考虑孙子节点int v1 = root->val;if (root->left)v1 += rob(root->left->left) + rob(root->left->right);if (root->right)v1 += rob(root->right->left) + rob(root->right->right);// 不偷当前节点(父节点),则可以考虑左右孩子节点int v2 = rob(root->left) + rob(root->right);valueMap[root] = max(v1, v2); // 记录当前节点能偷的最大金额return valueMap[root];}
};

此外,还可以采用动态规划的方法。本题是我做的第一道树形结构中运用动态规划的题目,参考了 Carl的解决方案 ,感觉很巧妙:

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

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

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

/// @brief 抢或不抢当前节点root,在以root为根的子树中能获得的最大金额
/// @param root(当前节点)
/// @return 大小为2的数组dp,dp[0]表示不偷root能得最大金额,dp[1]表示偷
vector<int> tryRob(TreeNode *root) {

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

所以 dp 数组的含义:

  • 下标为0记录(绝对)不偷该节点所得到的的最大金钱
  • 下标为1记录(考虑)偷该节点所得到的的最大金钱

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

那么长度为2的数组怎么标记树中每个节点的状态呢?

别忘了在递归的过程中,系统栈会保存每一层递归的参数

如果还不理解的话,就接着往下看,看到代码就理解了。

2️⃣ 确定终止条件

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

vector<int> dp(2, 0);
if (!root) return dp; // 空节点,抢不抢都没钱

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

3️⃣ 确定遍历顺序

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

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

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

代码如下:

// 下标0:不偷,下标1:偷
vector<int> left = tryRob(root->left); // 抢或不抢左孩子
vector<int> right = tryRob(root->right); // 抢或不抢右孩子
// 中

4️⃣ 确定单层递归的逻辑

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

如果是偷当前节点,那么左右孩子就不能偷,dp[1] = root->val + left[0] + right[0];如果对下标含义不理解就再回顾一下 dp 数组的含义

最后当前节点的状态就是 {dp[0], dp[1]} ; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

代码如下:

// 不抢当前节点,则其子节点可抢可不抢
dp[0] = max(left[0], left[1]) + max(right[0], right[1]);// 抢当前节点,则其子节点绝对不能抢
dp[1] = root->val + left[0] + right[0];

5️⃣ 举例推导dp数组

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

在这里插入图片描述

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

整体代码如下:

class Solution // 动态规划
{
private:/// @brief 抢或不抢当前节点root,在以root为根的子树中能获得的最大金额/// @param root(当前节点)/// @return 大小为2的数组dp,dp[0]表示不抢root能得最大金额,dp[1]表示抢vector<int> tryRob(TreeNode *root) {vector<int> dp(2, 0);if (!root)return dp; // 空节点,抢不抢都没钱vector<int> left = tryRob(root->left); // 抢或不抢左孩子vector<int> right = tryRob(root->right); // 抢或不抢右孩子// 不抢当前节点,则其子节点可抢可不抢dp[0] = max(left[0], left[1]) + max(right[0], right[1]);// 抢当前节点,则其子节点绝对不能抢dp[1] = root->val + left[0] + right[0];return dp;}public:int rob(TreeNode *root){vector<int> dp = tryRob(root);return max(dp[0], dp[1]);}
};

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • mysql的group by怎么用
  • 后端面试真题整理
  • Flask 安装和应用
  • LVGL之FFmpeg使用
  • 扬尘监测算法全套源码扬尘监测算法识别训练样本展示
  • 视频提取字幕的软件有哪些?5款高识别率工具任你选
  • git 主要命令
  • element的el-steps里的数据处理用switch 循环,处理的数据不要vue响应式的值
  • Leetcode 131.分割回文串 回溯 C++实现
  • Linux下qt程序缺少中文字库,中文显示为框框
  • 【Java设计模式】非循环访问者模式:简化对象交互
  • Git下载安装配置
  • Apache + Tomcat + ajp 协议配置
  • Android13禁用Settings里面的Force Stop 強制停止按钮
  • 浏览器精度问题
  • [Vue CLI 3] 配置解析之 css.extract
  • C++回声服务器_9-epoll边缘触发模式版本服务器
  • echarts花样作死的坑
  • iOS仿今日头条、壁纸应用、筛选分类、三方微博、颜色填充等源码
  • JavaScript 奇技淫巧
  • OSS Web直传 (文件图片)
  • 从0搭建SpringBoot的HelloWorld -- Java版本
  • 对JS继承的一点思考
  • 多线程事务回滚
  • 聊聊hikari连接池的leakDetectionThreshold
  • 码农张的Bug人生 - 初来乍到
  • 微信开放平台全网发布【失败】的几点排查方法
  • 我与Jetbrains的这些年
  • 译自由幺半群
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 06-01 点餐小程序前台界面搭建
  • 策略 : 一文教你成为人工智能(AI)领域专家
  • ​queue --- 一个同步的队列类​
  • ​力扣解法汇总946-验证栈序列
  • #include到底该写在哪
  • #WEB前端(HTML属性)
  • #window11设置系统变量#
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • ${factoryList }后面有空格不影响
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (PySpark)RDD实验实战——取最大数出现的次数
  • (zt)最盛行的警世狂言(爆笑)
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (附源码)ssm智慧社区管理系统 毕业设计 101635
  • (简单) HDU 2612 Find a way,BFS。
  • (利用IDEA+Maven)定制属于自己的jar包
  • (面试必看!)锁策略
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (数据结构)顺序表的定义
  • (原創) 如何刪除Windows Live Writer留在本機的文章? (Web) (Windows Live Writer)
  • **PHP分步表单提交思路(分页表单提交)
  • .md即markdown文件的基本常用编写语法
  • .net core 依赖注入的基本用发
  • .Net IE10 _doPostBack 未定义
  • .net mvc 获取url中controller和action