打家劫舍专题
打家劫舍
题目:
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 。
提示:
0 <= nums.length <= 1000 <= nums[i] <= 400
看题目我第一时间想到的是状态数组,f[i]表示偷窃第i间房屋的所能偷到的最大金额,g[i]表示不偷窃第i间房屋所能偷到的最大金额。
这样就好办了:假设小偷从后往前偷(只是循环i--用到,你也可以往后偷,那就是i++),偷窃第i间房屋就等于不偷窃下一间房屋的最大金额并且将第i间房屋的金额拿到手——f[i] = g[i+1] + nums[i];不偷窃第i间房屋就等于偷窃下一间或者不偷窃下一间房屋的最大金额——g[i] = Math.max(f[i+1],g[i+1])。
当然只有一间就直接偷窃就好,只有两间偷最多钱的就好,题目1 <= nums.length <= 100不用考虑nums==null的情况。
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 1){
return nums[0];
}
if(len == 2){
return Math.max(nums[0],nums[1]);
}
int[] f = new int[len];
int[] g = new int[len];
f[len-1] = nums[len-1]; //初始状态,f[len-1]就是只拿了nums[len-1]的金币,g[len-1]就是0
for(int i = len-2;i >= 0;i--){ //小偷从后往前偷
g[i] = Math.max(f[i+1],g[i+1]);
f[i] = nums[i] + g[i+1];
}
return Math.max(f[0],g[0]);
}
}
打家劫舍2
题目:
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 = [0]
输出:0
提示:
1 <= nums.length <= 1000 <= nums[i] <= 1000
状态数组照旧。
如果房屋是排成一排的,返回第0间房屋的最大金额即是答案,问题就已经解决了,但是题目说房屋围成一圈,犯难的是:状态数组里一个状态由上一个状态演变而来,那最开始最开始的初始状态如何定义?也即:最后一间房屋的f[len-1]、g[len-1]如何得出?
由于偷窃了第一间就无法偷窃最后一间,不偷窃第一间就可以偷窃最后一间,所以也就两种情况:[0,n-2]和[1,n-1],这两种情况的f[]和g[]都是完全不一样的,只需要分开建立就好。
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 1){
return nums[0];
}
if(len == 2){
return Math.max(nums[0],nums[1]);
}
// ============================================[0,n-2]
int[] f1 = new int[len];
int[] g1 = new int[len];
f1[len-2] = nums[len-2];
for(int i = len-3;i >= 0;i--){
g1[i] = Math.max(f1[i+1],g1[i+1]);
f1[i] = g1[i+1]+nums[i];
}
int res1 = Math.max(f1[0],g1[0]);
// ============================================[1,n-1]
int[] f2 = new int[len];
int[] g2 = new int[len];
f2[len-1] = nums[len-1];
for(int i = len-2;i >= 1;i--){
g2[i] = Math.max(f2[i+1],g2[i+1]);
f2[i] = g2[i+1]+nums[i];
}
int res2 = Math.max(f2[1],g2[1]);
return Math.max(res1,res2);
}
}
打家劫舍3
题目:
337. 打家劫舍 III
难度中等
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
由于树有多叉,无法像数组、链表那样单向遍历扫描一遍就结束,所以建议递归。
递归比较吃内存,不优化硬上的话会造成超限,比如这题用递归完全不做优化的话:
class Solution {
public int rob(TreeNode root) {
//f(i)表示偷窃第i个节点的最大金额,g(i)表示不偷窃第i个节点的最大金额
//f(i) = g(左孩子) + g(右孩子) + root.val; g(i) = Math.max(f(左孩子),g(左孩子)) + Math.max(f(右孩子),g(右孩子));
return Math.max(fbackroll(root),gbackroll(root));
}
//偷窃第root节点的最大金额(上往下遍历)
int fbackroll(TreeNode root){
if(root == null){
return 0;
}
return gbackroll(root.left) + gbackroll(root.right) + root.val;
}
//不偷窃第root节点的最大金额(上往下遍历)
int gbackroll(TreeNode root){
if(root == null){
return 0;
}
return Math.max(fbackroll(root.left),gbackroll(root.left)) + Math.max(fbackroll(root.right),gbackroll(root.right));
}
}
会在以下测试用例超限:
执行结果:
超出时间限制
最后执行的输入:
[79,99,77,null,null,null,69,null,60,53,null,73,11,null,null,null,62,27,62,null,null,98,50,null,null,90,48,82,null,null,null,55,64,null,null,73,56,6,47,null,93,null,null,75,44,30,82,null,null,null,null,null,null,57,36,89,42,null,null,76,10,null,null,null,null,null,32,4,18,null,null,1,7,null,null,42,64,null,null,39,76,null,null,6,null,66,8,96,91,38,38,null,null,null,null,74,42,null,null,null,10,40,5,null,null,null,null,28,8,24,47,null,null,null,17,36,50,19,63,33,89,null,null,null,null,null,null,null,null,94,72,null,null,79,25,null,null,51,null,70,84,43,null,64,35,null,null,null,null,40,78,null,null,35,42,98,96,null,null,82,26,null,null,null,null,48,91,null,null,35,93,86,42,null,null,null,null,0,61,null,null,67,null,53,48,null,null,82,30,null,97,null,null,null,1,null,null]
结果应该是对的,毕竟思路算清晰了。
要加记忆化搜索,也叫剪枝、查表。
什么是记忆化搜索?就是不要每次判断都是独立从头计算,而是利用数组记录下做过的数据,然后计算中就不用再次计算只需要查表就行了。剪枝、查表、记忆化搜索、动态规划都是这个原理。
/**
* 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 {
HashMap<TreeNode,Integer> f = new HashMap<>();
HashMap<TreeNode,Integer> g = new HashMap<>();
public int rob(TreeNode root) {
//f(i)表示偷窃第i个节点的最大金额,g(i)表示不偷窃第i个节点的最大金额
//f(i) = g(左孩子) + g(右孩子) + root.val; g(i) = Math.max(f(左孩子),g(左孩子)) + Math.max(f(右孩子),g(右孩子));
return Math.max(fbackroll(root),gbackroll(root));
}
//偷窃第root节点的最大金额(上往下遍历)
int fbackroll(TreeNode root){
if(root == null){
return 0;
}
int lres = g.getOrDefault(root.left,0);
int rres = g.getOrDefault(root.right,0);
int res = (lres == 0 ? gbackroll(root.left) : lres)
+
(rres == 0 ? gbackroll(root.right) : rres)
+
root.val;
f.put(root,res); //剪枝
return res;
}
//不偷窃第root节点的最大金额(上往下遍历)
int gbackroll(TreeNode root){
if(root == null){
return 0;
}
int flres = f.getOrDefault(root.left,0);
int glres = g.getOrDefault(root.left,0);
int frres = f.getOrDefault(root.right,0);
int grres = g.getOrDefault(root.right,0);
int res = Math.max(flres == 0 ? fbackroll(root.left) : flres ,glres == 0 ? gbackroll(root.left) : glres)
+
Math.max(frres == 0 ? fbackroll(root.right) : frres ,grres == 0 ? gbackroll(root.right) : grres);
g.put(root,res); //剪枝
return res;
}
}

浙公网安备 33010602011771号