leetCode 198.打家劫舍 动态规划入门:从记忆化搜索到递推

news2024/11/16 9:36:37

leetCode 198.打家劫舍 198. 打家劫舍 - 力扣(LeetCode)


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

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


动态规划的核心状态定义状态转移方程的思考方法,举“打家劫舍”为例,详细讲解如何通过回溯思路和子集型回溯思路来思考动态规划问题。同时,介绍如何优化回溯代码和使用递推思路来解决动态规划问题。

(一)状态定义?状态转移方程
  • 启发思路:选 或 不选 / 选哪个

对于这个题,先把它看做是一道回溯题要把一个大问题变成一个规模更小的子问题,从第一个房子或者最小一个房子开始思考,是最容易的,因为它们受到的约束是最小的。

比如考虑最后一个房子「选」还是「不选」,如果「不选」,那么问题就变成 n-1 个房子的问题。如果选,问题就变成 n-2 个房子的问题,不断这样去思考,就可以得到一棵搜索树了。

(二)回溯

由于在选的情况下,相邻的房子是不能选的,所以这里直接递归到 n-2 个房子,把刚才的思考过程再抽象一下:当我们枚举到第 i 个房子「选」「不选」的时候,就确定了递归参数中的 i,那么 dfs(i) 的含义是什么呢?就是从前 i 个房子中得到的最大金额和。如果「不选」第 i 个房子,问题就变成从 i-1 个房子中得到的最大金额和。如果选i 个房子,问题就变成从 i-2 个房子中得到的最大金额和。这样你就知道要往哪里递归了

  • Python代码:
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        def dfs(i):
            if i<0:
                return 0
            res = max(dfs(i-1),dfs(i-2)+nums[i]);
            return res
        return dfs(n-1)
  •  C++代码:
class Solution {
public:
    // 回溯
    int rob(vector<int>& nums) {
        int n = nums.size();
        function<int(int)>dfs = [&](int i) -> int {
            if(i<0) return 0;
            return max(dfs(i-1),dfs(i-2)+nums[i]);
        };
        return dfs(n-1);
    }
};
  • 超出时间限制 

定义DFS 或者 DP 数组的含义时,它只能表示从一些元素中算出结果,而不是从一个元素中算出结果,另外一点是:没有把得到的金额和作为递归的入参,而是把它当做了返回值,后面在写记忆化的时候,就明白为什么了?接着往下看~

(三) 递归搜索 + 保存计算结果 = 记忆化搜索

从图中可看出,dfs(2)算了两次,这两次计算的结果是一样的。那么干脆在第一次计算的时候,把计算结果存到一个 cache 数组或者哈希表中。这样在第二次算的时候,就可以直接返回 cache 里面保存的结果了

把递归的计算结果保存下来,那么下次递归到同样的入参时就直接返回先前保存的结果

  • 递归搜索 + 保存计算结果 = 记忆化搜索

可以看到优化后这颗搜索树只有 O(n) 个节点,因此时间复杂度也优化到了 O(n)

  • Python代码: 
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        # 用cache,它的原理是用一个hashmap记录入参和对应的返回值(对于这份代码,也可以用数组来实现)
        @cache
        def dfs(i):
            if i<0:
                return 0
            res = max(dfs(i-1),dfs(i-2)+nums[i]);
            return res
        return dfs(n-1)
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        cache = [-1] * n
        def dfs(i):
            if i<0:
                return 0
            if cache[i]!=-1:
                return cache[i]
            res = max(dfs(i-1),dfs(i-2)+nums[i])
            cache[i] = res
            return res
        return dfs(n-1)
  • C++代码:
class Solution {
public:
    int rob(vector<int> &nums) {
        int n = nums.size();
        vector<int> memo(n, -1); // -1 表示没有计算过
        // dfs(i) 表示从 nums[0] 到 nums[i] 最多能偷多少
        function<int(int)> dfs = [&](int i) -> int {
            if (i < 0) return 0; // 递归边界(没有房子)
            if (memo[i] != -1) return memo[i]; // 之前计算过
            return memo[i] = max(dfs(i - 1), dfs(i - 2) + nums[i]);
        };
        return dfs(n - 1); // 从最后一个房子开始思考
    }
};
class Solution {
public:
    // 记忆化递归
    int rob(vector<int>& nums) {
        int n = nums.size();
        vector<int> memo(n,-1);
        function<int(int)>dfs = [&](int i) -> int {
            if(i<0) return 0;
            int& res = memo[i];
            if(res != -1) return res;
            return res=max(dfs(i-1),dfs(i-2)+nums[i]);
        };
        return dfs(n-1);
    }
};
class Solution {
public:
    // 记忆化递归
    int rob(vector<int>& nums) {
        int n = nums.size();
        vector<int> memo(n+2,-1);
        function<int(int)>dfs = [&](int i) -> int {
            if(i<0) return 0;
            int &res = memo[i];
            if(res != -1) return res;

            int& x = memo[i+1];
            if(x == -1) x = dfs(i-1);
            
            int& y = memo[i+2];
            if(y == -1) y = dfs(i-2)+nums[i];

            return res=max(x,y);
        };
        return dfs(n-1);
    }
};
  • 时间复杂度:O(n),其中 n 为 nums 的长度
  • 空间复杂度:O(n)

(四)1:1 翻译成递推

  • 算 = 记忆化搜索
  • 算 = 递推

怎么把记忆化搜索改成递推呢?把 dfs 改成 f 数组,把 「递归」改成 「​​​​​​​​​​​​​​循环」 就好了。但这样写的话,需要对 i = 0  和 i = 1 的情况特殊处理,因为这里会产生负数下标。为了避免出现负数下标,你可以把 i 改成从 2 开始,也可以把这三处的 i 都加 2 ,得到如下式子


  • dfs(i) = max(dfs(i-1),dfs(i-2)+nums[i]));
  • f[i] = max(f[i-1],f[i-2] + nums[i]);
  • f[i+2] = max(f[i+1],f[i] + nums[i]);

  • Python代码:  
# 空间复杂度:O(n)
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        f = [0] * (n+2)
        for i,x in enumerate(nums):
            f[i+2] = max(f[i+1],f[i]+x);
        return f[n+1]
  • C++代码:
class Solution {
public: 
    // 1:1 翻译成递推:f[i+2] = max(f[i+1],f[i]+nums[i]);
    int rob(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n+2,0);
        for(int i=0;i<n;i++) 
            f[i+2] = max(f[i+1],f[i]+nums[i]);
        return f[n+1];
    }
};
  • 时间复杂度:O(n),其中 n 为 nums 的长度
  • 空间复杂度:O(n) 

思考:如何把空间复杂度优化成 O(1) 呢?

要计算 f(i) ,只需要直到它的 上一个 状态和 上上一个 状态的值,此外对于 f(i+1) 来说,f(i) 就变成它的上一个状态了,f(i-1) 就变成了它的上上一个状态了。那么用 f0 表示「上上一个」f1 表示「上一个」 就可以变成这个式子:


  • 当前 = max(上一个,上上一个 + nums[i])
  •  f0  表示 上上一个f1 表示 上一个
  • newF = max(f1,f0+nums[i])
  • f0 = f1
  • f1 = newF

算出 newF 之后,就要准备计算下一个了,此时f1就变成了上上一个newF 就变成了上一个

# 空间复杂度:O(1)
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        f0 = f1 = 0
        for i,x in enumerate(nums):
            new_f = max(f1,f0+x);
            f0=f1
            f1=new_f
        return f1
  • ​​​​​​​C++代码: 
class Solution {
public:
    // 空间优化
    int rob(vector<int>& nums) {
        int n = nums.size();
        int f0=0,f1=0;
        for(const int& x:nums) {
            int new_f = max(f1, f0 + x);
            f0 = f1;
            f1 = new_f;
        }
        return f1;
    }
};
  • 时间复杂度:O(n),其中 n 为 nums 的长度
  • 空间复杂度:O(1),仅用到若干额外变量 

参考和推荐文章、视频:

198. 打家劫舍 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/house-robber/solutions/2102725/ru-he-xiang-chu-zhuang-tai-ding-yi-he-zh-1wt1/动态规划入门:从记忆化搜索到递推_哔哩哔哩_bilibiliicon-default.png?t=N7T8https://www.bilibili.com/video/BV1Xj411K7oF/?spm_id_from=333.788&vd_source=a934d7fc6f47698a29dac90a922ba5a3

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

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

相关文章

地空智能国产化RTS无线探地雷达

广东地空智能科技有限公司推出的国产RTS无线探地雷达&#xff0c;是国内首款基于实时采样、高叠加技术的无线连接的探地雷达。RTS系列雷达是主机、天线一体化设计&#xff0c;32Bit数据&#xff0c;内部配置WiFi基站&#xff0c;无线传输距离≥100M&#xff0c;1000mAh进口电芯…

十九、类型信息(6)

接口和类型 interface 关键字的一个重要目标就是允许程序员隔离组件&#xff0c;进而降低耦合度。使用接口可以实现这一目标&#xff0c;但是通过类型信息&#xff0c;这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口&#xff1a; …

Java实验二类编程实验

1.编写一个代表三角形的类&#xff08;Triangle.java&#xff09;。 其中&#xff0c;三条边a,b,c&#xff08;数据类型为double类型&#xff09;为三角形的属性&#xff0c;该类封装有求三角形的面积和周长的方法。分别针对三条边为3、4、5和7、8、9的两个三角形进行测试&…

软件测试:postman使用总结

一、为何使用postman postman是一款简单高效的接口测试工具&#xff0c;能够很方便发送接口请求&#xff0c;易于保存接口请求脚本&#xff0c;postman提供接口响应数据比对功能&#xff0c;可以设置预期结果作断言&#xff0c;还能把测试用例放在一个集合中批量执行&#xff…

七月论文审稿GPT第二版:从Meta Nougat、GPT4审稿到mistral、llama longlora

前言 如此前这篇文章《学术论文GPT的源码解读与微调&#xff1a;从chatpaper、gpt_academic到七月论文审稿GPT》中的第三部分所述&#xff0c;对于论文的摘要/总结、对话、翻译、语法检查而言&#xff0c;市面上的学术论文GPT的效果虽暂未有多好&#xff0c;可至少还过得去&am…

图论问题建模和floodfill算法

目录 引入&#xff1a;leetcode695.岛屿的最大面积 分析与转换 一维二维转换 四联通 完整代码解答&#xff1a; 1&#xff09;显示的创建图解决问题的代码 2&#xff09;不显示的创建图解决此问题的代码 floodfill算法 定义 引入&#xff1a;leetcode695.岛屿的最大面…

精准测试:提高软件质量和用户满意度的利器

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

c语言练习(9周)(16~20)

输入12个一位整数&#xff0c;创建二维数组a[3][4]&#xff0c;显示二维数组及各列的平均值&#xff0c;平均值四舍五入到小数点后一位。 题干输入12个一位整数&#xff0c;创建二维数组a[3][4]&#xff0c;显示二维数组及各列的平均值&#xff0c;平均值四舍五入到小数点后一…

华为云服务器,在线安装MySQL

需求 在华为云服务器上&#xff0c;部署MySQL数据库&#xff0c;通过 公网IP 访问数据库。 通过 yum &#xff0c;在线安装MySQL&#xff1b;配置远程连接&#xff0c;开放3306端口&#xff0c;能够通过公网访问。 云服务器配置说明 本文所使用的 华为云服务器 配置如下。 …

有关常见的#define定义的函数的陷阱和修正(详解)

一、#define f(x) x*x #include<stdio.h> #define f(x) x*x int main() {int a6,b2,c;cf(a)/f(b);printf("f(a)%d\n",f(a));//6*6printf("f(b)%d\n",f(b));//2*2printf("f(b1)%d\n",f(b1));//21*21; printf("f(b2)%d\n",f(b2))…

如何回答好“测得怎么样了?”

有测试员抱怨开发很糟糕&#xff0c;但我们没办法要求开发在会写代码的同时还要把代码写好&#xff0c;没有过多的bug&#xff0c;因为这就是我们的工作&#xff1b;测试员吐槽自己的老板很较真&#xff0c;但我们没办法拒绝领导们的批评指责&#xff0c;因为批评代表我们还有继…

VFIO的使用及原理

vfio设备透传主要用于将设备直通给虚拟机以提高性能&#xff0c;本篇以一张网卡为例讲述VFIO设备的配置使用及底层原理。其中涉及的技术背景主要有linuxqemukvmvfio。 一、VFIO网卡的配置使用 1.host配置iommu 首先是宿主机host必须支持硬件虚拟化技术&#xff0c;如x86架构…

是谁家班主任还不知道 怎么发布期中成绩啊。

你知道吗&#xff1f;居然还有班主任不知道怎么发布期中成绩&#xff01; 发布成绩并不是一件难事&#xff0c;只需几个步骤&#xff0c;就能轻松搞定&#xff01; 给大家讲一下成绩查询是什么。成绩查询是指学生通过一定的方式&#xff0c;如输入学号、姓名等&#xff0c;在指…

小程序如何设置自动使用物流账号发货

小程序支持自动使用物流账号发货并生成运单号。商家需要与物流公司合作&#xff0c;获取物流账号&#xff0c;支持快递物流和同城外卖配送平台。具体方法请参考公众号之前发布的文章&#xff0c;例如可以搜索“快递账号”。 导入物流账号后&#xff0c;在小程序管理员后台->…

Ansible中的变量及加密

目录 1.变量命名 2.变量级别 3.变量设定和使用方式 在playbook中直接定义变量 在文件中定义变量 使用变量 设定主机变量和清单变量 目录设定变量 用命令覆盖变量 使用数组设定变量 ​编辑 注册变量 事实变量 ​编辑 魔法变量 JINJA2模板 j2模板书写规则 ​编辑 f…

【MySQL】 索引(上)

文章目录 1. 索引的概念2. MySQL与磁盘 的交互基本单位3. 建立共识4. 现象与结论如何理解mysql中page概念为什么 要采用page的方案 进行交互 而不是用多少加载多少&#xff1f; 5. 页目录为什么要引入 页目录概念单页情况多页情况使用B树 构建索引为什么不用其他数据结构为什么…

打造更智能的移动端平台,蚂蚁mPaaS5.0亮相云栖大会

11月2日&#xff0c;在云栖大会上&#xff0c;蚂蚁数科宣布mPaaS正式升级至5.0版本。mPaaS5.0融合了蚂蚁大模型框架下的多种算法能力&#xff0c;以AI智能引擎驱动移动端平台的运营、服务和体验管理实现全链路升级&#xff0c;助力金融机构及企业打造更加智能、更懂用户的移动端…

数据结构与算法:使用数组模拟环形队列Java版

文章目录 如何使用数组模拟队列环形队列逻辑分析自己写的听课笔记实现代码部分方法说明 如何使用数组模拟队列 不知道如何使用数组模拟队列的可以看上一篇文章 使用数组模拟队列点击跳转 环形队列逻辑分析 自己写的听课笔记 实现代码 package com.haimeng.queue;import java…

Java实验三类的继承与派生

1.定义一个Person类&#xff0c;包含姓名&#xff08;name&#xff09;、性别&#xff08;sex&#xff09;、年龄&#xff08;age&#xff09;等字段&#xff1b;继承Person类设计Teacher 类&#xff0c;增加职称&#xff08;pro&#xff09;、部门&#xff08;department&…

配置git并把本地项目连接github

一.配置git 1.下载git&#xff08;Git&#xff09;&#xff0c;但推荐使用国内镜像下载&#xff08;CNPM Binaries Mirror&#xff09; 选好64和版本号下载&#xff0c;全部点下一步 下载完成后打开终端&#xff0c;输入 git --version 出现版本号则说明安装成功 然后继续…