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 <= 100
- 0 <= nums[i] <= 400
本题的出发点是确定当前状态是否偷,而如果当前偷了,前一个和后一个就不能偷,所以也是和前面的状态有依赖关系,因此还是动态规划问题:
- 确定dp数组以及下标的含义:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
- 确定递推公式:如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。如果不偷第i房间,那么dp[i] = dp[i - 1],即考虑i-1房,不一定就偷第i-1个房,所以dp[i]=max(dp[i-1], dp[i-2]+nums[i])
- 初始化:因为需要用到i-1和i-2所以需要知道dp[0]和dp[1],dp[0]表示偷下标为0的屋子最多可以偷的金额,就是nums[0],dp[1]就是nums[0]和nums[1]的最大值
- 遍历顺序:从前向后,因为dp[i]需要根据i-1和i-2的状态推出来
class Solution {
public int rob(int[] nums) {
int[] dp =new int[nums.length];
dp[0]=nums[0];
if(nums.length==1) return 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];
}
}
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
提示:
- 1 <= nums.length <= 100
- 0 <= nums[i] <= 1000
本题和打家劫舍1的区别就在于房子是连着的,所以下标含义和递推逻辑不变,需要重新考虑初始化。这里多了一个需要判断的条件就是:i是否为最后一个房子,如果是,则需要考虑第一个房子偷不偷的问题,如果第一个房子没有被选,则i 就只跟i-1是否被选有关系。所以可以分两种情况分别计算,最后判断哪个值大,就返回哪个值。所以这里初始化两次,一次是第一间房一定不被抢,一个是第一间房一定被抢。
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
if (n == 1) return nums[0];
if (n == 2) return Math.max(nums[0], nums[1]);
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n - 1; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
int resultWithFirst =dp[n-2]; //选择第一间,则最后一个房子就不会被选,那么dp[n-1]=dp[n-2]
dp[0] = 0; // 重新初始化dp[0],第一间房子不被抢
dp[1] = nums[1]; // 重新初始化dp[1]
for (int i = 2; i < n; i++) {
// 第一间房子不被抢的情况下,最后一间可以被抢
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
int resultWithoutFirst = dp[n - 1];
return Math.max(resultWithoutFirst, resultWithFirst);
}
}
337.打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
本题思维和之前的打家劫舍是一样的,只不过数据形式换成了树,需要对树进行遍历,不能同时抢的条件变成抢了父亲节点就不能抢其对应的孩子节点,抢了孩子节点就不能抢对应的父亲节点。动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
对于遍历树,就又需要用到递归,下面对应递归三部曲:
- 确定递归函数的参数和返回值:要计算一个节点偷与不偷所得到的金钱,所以返回值就是一个长度为2的数组,对于动态规划就是返回一个dp数组,标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
- 确定终止条件:在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回0,这也相当于dp数组的初始化
- 确定遍历顺序:需要进行后续遍历,因为需要通过递归函数的返回值做下一步计算,遍历顺序为左右根
- 确定单层递归的逻辑:如果偷当前节点,就不考虑左右孩子,也就是val1=cur.root+left[0],如果不偷当前节点,判断val2 = max(left[0], left[1]) + max(right[0], right[1]);
所以本题的含义val[0]代表不偷,val[1]代表偷,最后判断val[0]和val[1]谁大
class Solution {
public int[] traversal(TreeNode root){
int[] val=new int[2];
if(root==null) return val;
int[] left= traversal(root.left);
int[] right=traversal(root.right);
//不偷
val[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
//偷
val[1]=root.val+left[0]+right[0];
return val;
}
public int rob(TreeNode root) {
int[] val=traversal(root);
return Math.max(val[0], val[1]);
}
}