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

本文主要是介绍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]);}
};

这篇关于24暑假算法刷题 | Day39 | 动态规划 VII | LeetCode 198. 打家劫舍,213. 打家劫舍 II,337. 打家劫舍 III的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

Mybatis嵌套子查询动态SQL编写实践

《Mybatis嵌套子查询动态SQL编写实践》:本文主要介绍Mybatis嵌套子查询动态SQL编写方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、实体类1、主类2、子类二、Mapper三、XML四、详解总结前言MyBATis的xml文件编写动态SQL

SpringBoot实现Kafka动态反序列化的完整代码

《SpringBoot实现Kafka动态反序列化的完整代码》在分布式系统中,Kafka作为高吞吐量的消息队列,常常需要处理来自不同主题(Topic)的异构数据,不同的业务场景可能要求对同一消费者组内的... 目录引言一、问题背景1.1 动态反序列化的需求1.2 常见问题二、动态反序列化的核心方案2.1 ht

golang实现动态路由的项目实践

《golang实现动态路由的项目实践》本文主要介绍了golang实现动态路由项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习... 目录一、动态路由1.结构体(数据库的定义)2.预加载preload3.添加关联的方法一、动态路由1

使用雪花算法产生id导致前端精度缺失问题解决方案

《使用雪花算法产生id导致前端精度缺失问题解决方案》雪花算法由Twitter提出,设计目的是生成唯一的、递增的ID,下面:本文主要介绍使用雪花算法产生id导致前端精度缺失问题的解决方案,文中通过代... 目录一、问题根源二、解决方案1. 全局配置Jackson序列化规则2. 实体类必须使用Long封装类3.

Python Selenium动态渲染页面和抓取的使用指南

《PythonSelenium动态渲染页面和抓取的使用指南》在Web数据采集领域,动态渲染页面已成为现代网站的主流形式,本文将从技术原理,环境配置,核心功能系统讲解Selenium在Python动态... 目录一、Selenium技术架构解析二、环境搭建与基础配置1. 组件安装2. 驱动配置3. 基础操作模

Springboot实现推荐系统的协同过滤算法

《Springboot实现推荐系统的协同过滤算法》协同过滤算法是一种在推荐系统中广泛使用的算法,用于预测用户对物品(如商品、电影、音乐等)的偏好,从而实现个性化推荐,下面给大家介绍Springboot... 目录前言基本原理 算法分类 计算方法应用场景 代码实现 前言协同过滤算法(Collaborativ

慢sql提前分析预警和动态sql替换-Mybatis-SQL

《慢sql提前分析预警和动态sql替换-Mybatis-SQL》为防止慢SQL问题而开发的MyBatis组件,该组件能够在开发、测试阶段自动分析SQL语句,并在出现慢SQL问题时通过Ducc配置实现动... 目录背景解决思路开源方案调研设计方案详细设计使用方法1、引入依赖jar包2、配置组件XML3、核心配

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

SpringBoot基于配置实现短信服务策略的动态切换

《SpringBoot基于配置实现短信服务策略的动态切换》这篇文章主要为大家详细介绍了SpringBoot在接入多个短信服务商(如阿里云、腾讯云、华为云)后,如何根据配置或环境切换使用不同的服务商,需... 目录目标功能示例配置(application.yml)配置类绑定短信发送策略接口示例:阿里云 & 腾

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各