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

news2024/9/24 19:24:42

目录

  • 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.coloradmin.cn/o/2070960.html

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

相关文章

初出茅庐:怎样获得实习机会——之找到一份工作

如果你刚开始工作&#xff0c;找到工作的最好和最简单的方法之一就是【实习】&#xff1b;许多技术类公司只雇佣实习生或者经验丰富的软件开发者&#xff0c;实习为公司提供了一个独一无二的机会&#xff0c;使其可以在雇用潜在员工之前对他们进行充分的评估。但实习的机会不是…

Pytorch升级之旅——基础概念

目录 一、人工智能简史 三次浪潮 DL,ML,AI三者之间的关系 二、模型评价指标 混淆矩阵 Overall Accuracy ​编辑 Average accuracy Kappa系数 Recall Precision F1 PR曲线 置信度 IOU AP mAP 三、常用包Numpy、pandas、matplotlib Numpy pandas matplotlib…

二叉树【2】遍历

先中后序 先序遍历&#xff1a;根左右 中序遍历&#xff1a;左跟右 后序遍历&#xff1a;左右跟 例图 先序性质 中序性质 后序性质 先序中序确定二叉树 后序中序确定二叉树 先序后序不能确定。 层序&#xff1a;广度优先搜索 如果需要修改树的元素&#xff0c;则需no…

机器学习(第六关--文本特征抽取)

以下内容&#xff0c;皆为原创&#xff0c;制作实属不易&#xff0c;感谢大家的观看和关注。 在此真诚的祝愿大家&#xff0c;生活顺顺利利&#xff0c;身体健健康康&#xff0c;前途似锦。 第一关&#xff1a;机器学习概念和流程http://t.csdnimg.cn/IuHh4第二关&#xff1a;…

【功能实现】Vue3中导航栏效果实现

功能需求&#xff1a; 点击导航后会跳转到相应的页面当页面在顶部时&#xff0c;导航栏模块背景是透明的当页面向下滚动超过150px时&#xff0c;导航栏背景为白色 效果实现&#xff1a; 1.准备列表 将导航栏区域的类名设置为动态&#xff0c;即通过判断isTransparent是否存在…

浅谈【数据结构】链表之其他形态

目录 1、带头结点的链表 2、创建步骤 3、循环链表 3.1创建循环链表 3.2循环链表的遍历 3.3链表中的闭环 4、静态链表 4.1静态链表初始化 谢谢帅气美丽且优秀的你看完我的文章还要点赞、收藏加关注 没错&#xff0c;说的就是你&#xff0c;不用再怀疑&#xff01;&#…

在群晖上安装Git客户端

什么是 Git &#xff1f; Git 是一个分布式版本控制系统&#xff0c;提供了命令行界面&#xff08;CLI&#xff09;以及图形用户界面&#xff08;GUI&#xff09;两种方式进行操作。与 CVS、Subversion 一类的集中式版本控制工具不同&#xff0c;它采用了分布式版本库的作法&am…

LinkedList添加和删除方法的源码分析超详细

我们基于下面这段代码进行分析 进入add方法后调用的是linkLast方法 这是add方法真正的底层逻辑&#xff0c;首先将last值赋给l节点&#xff0c;此时的l为null&#xff0c;然后再创建新节点的时候将l作为前向指针传入&#xff0c;将节点中的next指针也赋值为null&#xff0c;之后…

PHP反序列化二

1.反序列化基础利用 2.pop链 POP链: POP(面向属性编程&#xff09;链是指从现有运行环境中寻找一系列的代码或指令调用&#xff0c;然后根据需求构造出一组连续的调用链。 反序列化利用就是要找到合适的POP链。其实就是构造一条符合原代码需求的链条&#xff0c;去找到可以控…

基于YOLOv8的无人机高空红外(HIT-UAV)检测算法,新的混合型扩张型残差注意力(HDRAB)助力涨点(二)

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文内容&#xff1a;针对基于YOLOv8的无人机高空红外&#xff08;HIT-UAV&#xff09;检测算法进行性能提升&#xff0c;加入各个创新点做验证性试验。 &#x1f4a1;&#x1f4a1;&#x1f4a1;一种新的混合型扩张型残差注意力(HDRAB…

Springboot基础------控制器、过滤器、拦截器、监视器

控制器 控制器用于接收和响应用户发送的请求&#xff0c;一个控制器类需要被Controller注解&#xff1a;&#xff08;即controller层&#xff09; Controller class TestController { }注&#xff1a; RestController实际上是Controller和ResponseBody的简化版&#xff0c;被…

数据仓库系列 3:数据仓库的主要组成部分有哪些?

你是否曾经好奇过,当你在网上购物或使用手机应用时,背后的数据是如何被存储和分析的?答案就在数据仓库中。本文将为你揭开数据仓库的神秘面纱,深入探讨其核心组成部分,以及这些组件如何协同工作,将海量数据转化为有价值的商业洞察。 目录 引言:数据仓库的魔力1. 数据源和数据…

STM32 | STM32 FLASH第十二天(实现代码STM32CUBEMX)

点击上方"蓝字"关注我们 01、FLASH >>> 1、STM32 FLASH Flash内部存储器,内存器件的一种,是一种非易失性( Non-Volatile )内存。 flash闪存是非易失存储器,可以对称为块的存储器单元块进行擦写和再编程。任何flash器件的写入操作只能在空或已擦除的单元…

mac 虚拟机PD19运行E-prime实验遇到E-prime unable to set display mode:0*80004001问题解决

作者&#xff1a;50% unable to set display mode问题 总结&#xff1a; 1. 修改该Experiment的Devices中的Dispaly为640*680,或800*600。 2. 右键开始菜单中的E-Studio程序&#xff0c;打开文件所在位置&#xff0c;然后右键该文件&#xff0c;选择属性-兼容性&#xff0c;勾选…

【内网渗透】ICMP隧道技术,ICMP封装穿透防火墙上线MSF/CS

~ 会当凌绝顶&#xff0c;一览众山小 ~ 前言 博客主页&#xff1a;h0ack1r丶羽~ 从0到1~ 渗透测试中&#xff0c;如果攻击者使用各类上层隧道(例如&#xff1a;HTTP隧道、DNS隧道、常规正/反向端口转发等)进行的操作都失败了&#xff0c;常常会通过ping命令访问远程计算机&am…

医疗多模态大模型是什么?医学多模态模型总结:算法其实很简单,拼的就是硬件算力的问题!多模态大模型(医疗影像分析)

概念 医学多模态大模型是指利用多种不同的医学数据源和模型&#xff0c;通过深度学习和人工智能技术&#xff0c;构建一个综合性的大型模型&#xff0c;以实现更加准确和全面的医学数据分析和预测。 这种模型可以同时处理多种医学数据类型&#xff0c;如医学图像、病历文本、…

自动驾驶---各大车企的端到端之旅

1 背景 端到端技术的落地速度确实有些超出预料&#xff0c;随着以ChatGPT为代表的AI大模型的快速发展&#xff0c;使得自动驾驶系统也能够像人一样进行“思考”&#xff0c;推动了自动驾驶技术的迭代升级。 特斯拉端到端技术的落地&#xff0c;自动驾驶在这一技术上的变化使得自…

<C++> 二叉搜索树

目录 二叉搜索树 1. 概念 2. 二叉搜索树操作 2.1 基础结构 2.2 非递归版 1. 查找 2. 插入 3. 删除 2.3 递归版 1. 查找 2. 插入 3. 删除 2.4 拷贝构造函数 2.5 赋值运算符重载 2.6 析构函数 2.7 完整代码 3. 二叉搜索树的应用 4. 二叉搜索树的性能 二叉搜索树 1. 概念 二叉搜索…

机器学习算法那些事 | 这是我见过最通俗易懂的SVD(奇异值分解)算法介绍

本文来源公众号“机器学习算法那些事”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;这是我见过最通俗易懂的SVD&#xff08;奇异值分解&#xff09;算法介绍 线性代数是机器学习领域的基础&#xff0c;其中一个最重要的概念是奇…

手机号归属地查询如何用Java进行调用

一、什么是手机号归属地查询接口&#xff1f; 手机号归属地查询接口又叫手机号归属地、手机号信息查询、手机号查询&#xff0c;通过手机号查询归属地信息、是否虚拟运营商等。该接口可支持三大运营商&#xff0c;移动、电信、联通等。 二、手机号归属地查询接口适用场景有哪…