本文通过对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
地址
解题方法
java
class Solution {
public int rob(int[] nums) {
if (nums == null) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
// dp[i] 在不发动警报的前提下,从[0,i]之间的房子中能获取到最大金额数为dp[i]
int[] dp = new int[nums.length];
// 初始化dp数组
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.length - 1];
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是数组长度
- 空间复杂度:O(n),其中 n 是数组长度
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
地址
解题方法
java
class Solution {
public int rob(int[] nums) {
if (nums == null) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
int result1 = rob1(nums, 0, nums.length - 2);
int result2 = rob1(nums, 1, nums.length - 1);
return Math.max(result1, result2);
}
public int rob1(int[] nums, int startIndex, int endIndex) {
if (nums == null) {
return 0;
}
if (endIndex == startIndex) {
return nums[endIndex];
}
// dp[i] 在不发动警报的前提下,从[0,i]之间的房子中能获取到最大金额数为dp[i]
int[] dp = new int[nums.length];
// 初始化dp数组
dp[startIndex] = nums[startIndex];
dp[startIndex+1] = Math.max(nums[startIndex], nums[startIndex + 1]);
for (int i = startIndex + 2; i < endIndex + 1; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[endIndex];
}
}
复杂度分析
-
时间复杂度:O(n),其中 n 是数组长度
-
空间复杂度:O(n),其中 n 是数组长度
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, 10^4]
范围内 0 <= Node.val <= 10^4
中文版地址
解题方法
java
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
//dp[0] 不偷当前节点得到的金额;dp[1] 偷当前节点得到的金额
if (root == null) {
return 0;
}
int[] dp = traveral(root);
return Math.max(dp[0], dp[1]);
}
private int[] traveral(TreeNode cur) {
int[] dp = {0, 0};
if (cur == null) {
return dp;
}
int[] leftResult = traveral(cur.left);
int[] rightResult = traveral(cur.right);
dp[0] = Math.max(leftResult[0], leftResult[1]) + Math.max(rightResult[0], rightResult[1]);
dp[1] = leftResult[0] + rightResult[0] + cur.val;
return dp;
}
}
复杂度分析
-
时间复杂度:O(n),其中 n 是二叉树的节点数
-
空间复杂度:O(n),其中 n 是二叉树的节点数,主要是递归栈占用的空间,最坏情况:二叉树为线性结构时O(n),最优情况:二叉树为平衡树时为O(logn)
总结
我们来对比下这三道题:
首先它们背景相同,都要求相邻的房屋不能同时被偷盗,计算不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
其次就是差异点:198是数组,213是环,337是二叉树,在做这三道题时我是有种越来越难的感觉(不知道大部分小伙伴是不是跟我一样=_=),其实主要就是数据结构使得考虑的情形越来越复杂,于是题目就越来越难。
其实,198是一道很典型的动态规划问题,即取前一状态的最优解,如果做完198再去做213会觉得213的方法很巧妙,213的关键在于当数组变为环以后,第一个值跟最后一个值就变成了相邻的,于是不能同时取,那么取哪个呢?
自然是取哪个大就取哪个,于是就可以将这一个问题拆成两个问题:
- 取第一个元素不取最后一个元素时,能够偷窃到的最高金额
- 不取第一个元素取最后一个元素时,能够偷窃到的最高金额
这就把复杂的环分解成了两个简单的普通数组,于是我们就可以通过执行两次198分别求出这两个数组的能够偷窃到的最高金额,然后取最大值。
而337则需要熟悉二叉树的遍历了,这里比较难想到的是利用一个dp[0]
来表示不取当前节点的最大金额和利用dp[1]
表示取当前节点的最大金额(我也是看了卡哥的解析才明白的),又利用递归栈分别可以存局部变量的特点,迭代后序遍历二叉树(因为需要通过子节点的金额推断父节点的金额,所以徐需要后序遍历),每次获取当前二叉树的最大金额,最后推到根节点。