LeetCode 198:打家劫舍——Java 动态规划详解

一、题目描述

给定一个非负整数数组 nums,其中 nums[i] 表示第 i 间房屋存放的金额。相邻房屋装有相互连通的报警系统,如果同一晚上偷窃两间相邻房屋,就会触发警报。

要求计算在不触发警报的情况下,一晚上最多能够获得多少金额。

例如:

复制代码
输入:nums = [1,2,3,1]
输出:4

可以选择第 1 间和第 3 间房屋,获得 1 + 3 = 4

再看一个例子:

复制代码
输入:nums = [2,7,9,3,1]
输出:12

可以选择金额为 291 的三间房屋,最终获得 12

这道题的关键限制是:相邻房屋不能同时选择。每走到一间新房屋,都需要决定是偷还是不偷,这是一道经典的动态规划问题。

二、为什么使用动态规划

最直接的思路,是枚举每间房屋偷或不偷的所有组合,再从合法方案中选择最大值。但每间房屋都有两种选择,暴力搜索的规模会快速增长。

实际上,当我们考虑第 i 间房屋时,不需要知道前面具体偷了哪些房屋,只需要知道:

  • 偷窃前面若干间房屋能够获得的最大金额;

  • 如果选择当前房屋,前一间房屋就不能选择。

也就是说,大问题的答案依赖于规模更小的子问题,而且这些子问题会被反复使用,因此适合采用动态规划保存结果。

三、定义动态规划状态

定义:

复制代码
dp[i] 表示偷窃前 i 间房屋能够获得的最高金额

需要特别注意,i 表示房屋数量,不是 nums 的数组下标。

例如:

  • dp[0]:没有房屋可选时的最高金额;

  • dp[1]:只考虑 nums[0] 时的最高金额;

  • dp[2]:考虑 nums[0]nums[1] 时的最高金额;

  • dp[n]:考虑全部 n 间房屋时的最高金额。

因此,在计算 dp[i] 时,当前房屋在数组中的下标是 i - 1,对应金额为:

复制代码
nums[i - 1]

这种定义可以让 dp[0] 自然表示空区间,也能减少边界判断。

四、推导状态转移公式

计算前 i 间房屋的最高金额时,需要决定是否偷第 i 间房屋。

1. 不偷当前房屋

如果不偷第 i 间房屋,那么答案与只考虑前 i - 1 间房屋时相同:

复制代码
dp[i - 1]

2. 偷当前房屋

如果偷第 i 间房屋,就不能偷与它相邻的第 i - 1 间房屋。因此,只能在前 i - 2 间房屋的最优结果上,加上当前房屋金额:

复制代码
dp[i - 2] + nums[i - 1]

在两种选择中取最大值,就得到状态转移公式:

复制代码
dp[i] = Math.max(
        dp[i - 1],
        dp[i - 2] + nums[i - 1]
);

可以将它记成一句话:

不偷当前房屋,就继承前一间的最优解;偷当前房屋,就使用前两间的最优解加上当前金额。

五、确定初始状态

动态规划必须先确定最小规模问题的答案。

1. 没有房屋

没有房屋可偷,最高金额自然是 0

复制代码
dp[0] = 0;

2. 只有一间房屋

房屋金额都是非负整数,因此只有一间房屋时,选择它就是最优方案:

复制代码
dp[1] = nums[0];

i = 2 开始,就可以使用状态转移公式依次计算后续结果。

六、示例完整推演

以数组 [1,2,3,1] 为例,共有 4 间房屋,因此创建长度为 5 的 DP 数组。

1. 初始化

复制代码
dp[0] = 0
dp[1] = 1

此时:

复制代码
dp = [0,1,0,0,0]

2. 计算 dp2

当前房屋金额为 nums[1] = 2

复制代码
不偷:dp[1] = 1
偷:dp[0] + nums[1] = 0 + 2 = 2

选择较大值:

复制代码
dp[2] = 2

3. 计算 dp3

当前房屋金额为 nums[2] = 3

复制代码
不偷:dp[2] = 2
偷:dp[1] + nums[2] = 1 + 3 = 4

所以:

复制代码
dp[3] = 4

4. 计算 dp4

当前房屋金额为 nums[3] = 1

复制代码
不偷:dp[3] = 4
偷:dp[2] + nums[3] = 2 + 1 = 3

最终:

复制代码
dp[4] = 4

完整 DP 数组为:

复制代码
[0,1,2,4,4]

因此答案为 4

这里还能发现一个规律:dp[i] 不一定表示必须偷第 i 间房屋,而是表示考虑前 i 间房屋后能够获得的最高金额。

七、完整 Java 代码

下面的代码与截图中的一维 DP 数组思路一致:

复制代码
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;

        // 防御性处理;LeetCode 原题保证数组至少包含一个元素
        if (n == 0) {
            return 0;
        }

        // dp[i] 表示偷窃前 i 间房屋能获得的最高金额
        int[] dp = new int[n + 1];

        // 初始状态
        dp[0] = 0;
        dp[1] = nums[0];

        for (int i = 2; i <= n; i++) {
            // 选择一:不偷第 i 间房屋,继承 dp[i - 1]
            // 选择二:偷第 i 间房屋,金额为
            //         dp[i - 2] + nums[i - 1]
            dp[i] = Math.max(
                    dp[i - 1],
                    dp[i - 2] + nums[i - 1]
            );
        }

        return dp[n];
    }
}

八、为什么返回 dpn

数组中共有 n 间房屋,而 dp[i] 表示考虑前 i 间房屋的最优结果,所以考虑全部房屋后的答案就是:

复制代码
return dp[n];

不能直接返回 dp[n - 1],因为那只表示考虑前 n - 1 间房屋,没有包含最后一间房屋带来的选择。

九、复杂度分析

程序只遍历一次数组,每个状态都在常数时间内完成计算,因此时间复杂度为:

复制代码
O(n)

使用长度为 n + 1 的 DP 数组保存所有子问题结果,因此空间复杂度为:

复制代码
O(n)

虽然本题可以进一步使用两个变量将空间复杂度优化为 O(1),但本文严格采用截图中的一维 DP 数组方案,便于理解状态定义和转移过程。

相关推荐
2601_963870221 小时前
【计算机毕业设计】基于Spring Boot的考研信息交流平台的设计与实现
java·spring boot·后端
大模型丫丫1 小时前
Spring Boot 入门指南:从零开始构建你的第一个应用
java·spring boot·后端
拾光Ծ1 小时前
Linux五种I/O模型与fcntl非阻塞控制
java·linux·服务器·i/o模型
半旧5182 小时前
【Java Web02】WEB服务器内部实现技术揭秘
java·服务器·前端
gis开发之家2 小时前
日志怎么打才专业?Spring Boot 4 日志体系与 Logback 配置(生产级实战)
java·spring boot·后端·logback
SaaS发言人2 小时前
渠道数字化解决方案横评:谁更懂快消
java·大数据·人工智能
JL152 小时前
Java并发编程面试全攻略-从synchronized到AQS底层原理
java·开发语言·面试·并发编程
ZC跨境爬虫2 小时前
LeetCode 119. 杨辉三角 II(原地更新优化详解 + Java Python 实现)
java·python·leetcode