LeetCode 第 198. House Robber(打家劫舍),题目难度 Easy。
一. 题目要求
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
二. 解题思路 & 代码
本题主要考察的是动态规划,动态规划相关的题目,一般来说有两个特点:
- 当前步骤都是之前状态的叠加,因此一般可以通过递归来解决
- 在当前步骤往往会面临一个 “TO BE OR NOT TO BE” 场景
回到本题,劫匪在走到每个房间时,都面临过两个抉择:
- 打劫
- 不打劫
用 i
表示当前房间的索引,因为不能同时打劫相邻的两个房间,那么在当前位置 i
处可以打劫到的最高金额就有两种情况:
- 打劫到前一房间
i-1
所获得金额 - 打劫到
i-2
处房间的最大金额 + 当前房间的所获金额
用递归公式总结就是:
max[i] = Math.max(max[i-1], max[i-2] + current)
得到了递归公式,整体代码就很简单了,代码如下:
// 解法 1:递归求解
class Solution {
private int[] nums;
private int result = Integer.MIN_VALUE;
public int rob(int[] nums) {
this.nums = nums;
int len = nums.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
return Math.max(helper(len - 1), helper(len - 2));
}
private int helper(int index){
if (index < 0) {
return 0;
}
return Math.max(helper(index - 2) + nums[index], helper(index - 1));
}
}
上面的代码逻辑是没有问题的,但是会存在重复计算的问题,导致代码运行超时。改进策略就是空间换时间,加一个 Map 记录每个位置处的最大收益值,改进后的代码如下:
// 解法 2:递归求解,缓存中间值
class Solution {
private Map<Integer, Integer> map = new HashMap<>();
public int rob(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
return Math.max(helper(len - 1), helper(len - 2));
}
private int helper(int index){
if (index < 0) {
return 0;
}
if (!map.containsKey(index)) {
int max = Math.max(helper(index - 2) + nums[index], helper(index - 1));
map.put(index, max);
}
return map.get(index);
}
}
通过空间换时间,算法整体的时间复杂度为 O(N),空间复杂度为 O(N),每个索引处都会计算一遍,另外会占用额外的 Map 空间。
上面算法还是有继续优化的空间的,因为使用了 Map 的原因,每次递归运算至少有 1 次 Map 的判断操作和 1 次读取操作,并且全部运算下来目测会有 N 次 put 操作。我们知道递归操作一般都可以转换为遍历操作,回到题目,如果我们先计算 i
和 i+1
处的值,那么计算 i + 2
处的值时就不用额外的判断操作了,优化后代码如下:
// 解法 3:正向迭代,缓存中间值
class Solution {
private Map<Integer, Integer> map = new HashMap<>();
public int rob(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
if (len == 2) {
return Math.max(nums[0], nums[1]);
}
// 先存储好打劫到 0 和 1 处的最大收益值
map.put(0, nums[0]);
map.put(1, Math.max(nums[0], nums[1]));
for (int i = 2; i < len; i ++) {
int max = Math.max(map.get(i-2)+nums[i], map.get(i-1));
map.put(i, max);
}
return map.get(len-1);
}
}
上面使用的 Map 是用索引作为 key 的,我们可以用数组而不是 Map 进行临时数据的存储,这样可与进一步提高性能,使用数组实现的代码如下:
class Solution {
public int rob(int[] nums) {
int len = nums.length;
int tmpNums[] = new int[len];
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
if (len == 2) {
return Math.max(nums[0], nums[1]);
}
tmpNums[0] = nums[0];
tmpNums[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < len; i ++) {
int max = Math.max(tmpNums[i-2]+nums[i], tmpNums[i-1]);
tmpNums[i] = max;
}
return tmpNums[len-1];
}
}
三. 解题后记
题目难度不大,重点在于想清楚动态规划解题的一个关键思路:
当前步骤都是之前状态的叠加,一般可以通过递归来解决
关于本题目,其 discuss 模块有一篇总结非常赞,自己也是读了这篇总结之后整理的做题思路,非常值得一看:
老铁,都看到这了来一波点赞、评论、关注三连可好
我是 AhriJ邹同学,前后端、小程序、DevOps 都搞的炸栈工程师。博客持续更新,如果觉得写的不错,欢迎来一波老铁三连,不好的话也欢迎指正,互相学习,共同进步。