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

ops/2024/9/23 22:55:32/

目录

  • 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]);}
};

http://www.ppmy.cn/ops/98676.html

相关文章

大话设计模式解读06-原型模式

上篇文章&#xff0c;介绍了《大话设计模式》的第8章——工厂方法。 本篇&#xff0c;来介绍《大话设计模式》的第9章——原型模式。并通过C代码实现实例代码的功能。 1 原型模式 原型模式&#xff08;Prototype&#xff09;&#xff1a;用原型实例指定创建对象的种类&#x…

【Java】从零到一使用Feign与Sentinel (详细图解)

Java系列文章目录 补充内容 Windows通过SSH连接Linux 第一章 Linux基本命令的学习与Linux历史 文章目录 Java系列文章目录一、前言二、学习内容&#xff1a;三、问题描述四、解决方案&#xff1a;4.1 认识依赖4.1.1 Feign依赖4.1.2 Sentinel依赖4.1.3 负载均衡依赖4.2 父子项目…

小程序-公告滚动

<view class"bg_box icon_nav_box" style"margin-top: -70rpx;position: relative;"><view class"flex flex_y_center"><view class"c_red f32" style"font-style: italic;flex-shrink:0 ;">最新公告<…

【Leetcode 1805 】 字符串中不同整数的数目—— 双指针

给你一个字符串 word &#xff0c;该字符串由数字和小写英文字母组成。 请你用空格替换每个不是数字的字符。例如&#xff0c;"a123bc34d8ef34" 将会变成 " 123 34 8 34" 。注意&#xff0c;剩下的这些整数为&#xff08;相邻彼此至少有一个空格隔开&am…

【PGCCC】PostgreSQL中的内存表架构与实现

在数据库系统中&#xff0c;内存表&#xff08;In-Memory Tables&#xff09;作为一种高效的数据存储方式&#xff0c;可以显著提升数据库的响应速度&#xff0c;特别是在需要频繁读写的场景下。PostgreSQL作为一款功能强大的开源数据库&#xff0c;也为我们提供了创建和使用内…

淘宝(天猫)商品详情数据接口在自有电商平台的应用!

在电商市场的日益成熟下&#xff0c;越来越多的电商参与者上线了自主研发的电商平台。这主要是因为&#xff0c;在电商销售中&#xff0c;品牌在自有电商平台售卖商品的优势颇多&#xff1a; 自有的电商平台能够赋予品牌更大的灵活性和自由度等&#xff0c;品牌商品销售时无需…

windows C++-WRL技术(一)

Windows 运行时 C 模板库 (WRL) 是一个提供低级别方式来创作和使用 Windows运行时组件的模板库。 WRL 现在已由 C/WinRT 取代。C/WinRT 是 Windows 运行时 API 的标准 C17 语言投影。 从版本 1803 (10.0.17134.0) 起&#xff0c;C/WinRT 在 Windows SDK 中提供。 C/WinRT 完全…