你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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 。
动态规划的思考
递归 不通过不知道为啥
class Solution {
public int rob(int[] nums) {
if(nums==null || nums.length==0) return 0;
return robHelper(nums, 0);
}
int res=0;
public int robHelper(int[] nums, int index){
if(index>=nums.length) return 0;
for(int i=index; i<nums.length;i++){
res=Math.max(res, nums[i]+robHelper(nums, i+2));
}
return res;
}
}
动态规划 6.5 重新写一遍
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
if(nums.length == 1) return nums[0];
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];
}
}
参考labuladong写的空间复杂度O(1)
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int n = nums.length;
//记录 dp[i+1] 和 dp[i+2]
int dp_i_1 = 0, dp_i_2 = 0; // 这样写 循环时避免数组越界,可以自动补上,不用特殊情况判断
int dp_i = 0;
for (int i = 0; i < n; i++) {
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
}
动态规划自己的思路
dp【i】存的是前 i 个数 最大值
然后试了半天整边界值终于通过啦
class Solution {
public int rob(int[] nums) {
if(nums.length==0) return 0;
int[] dp=new int[nums.length+1];
dp[0]=nums[0];
dp[1]= nums.length>1? Math.max(nums[0],nums[1]): nums[0];
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];
}
}
这样判断会比较麻烦,看到标准答案是:
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 0)
return 0;
int[] dp = new int[len + 1];
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2; i <= len; i++) {
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);
}
return dp[len];
}
这样少了一些判断
更好
lee213打家劫舍2
2020.6.5 改进
只是分两种情况,后面的函数还是上一题的思路
class Solution {
public int rob(int[] nums) {
int n= nums.length ;
if(nums==null || n == 0) return 0;
if(n==1) return nums[0];// 特殊判断
return Math.max(robHelper(nums,0,n-1),robHelper(nums,1,n));
}
private int robHelper(int[] nums, int start, int end){
int n = nums.length;
//记录 dp[i+1] 和 dp[i+2]
int dp_i_1 = 0, dp_i_2 = 0; // 记录 dp[i]
int dp_i = 0; // 不用管base case
for (int i = start; i < end; i++) {
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
}
思路
环状排列意味着第一个房子和最后一个房子中只能选择一个偷窃,因此可以把此环状排列房间问题约化为两个单排排列房间子问题:
在不偷窃第一个房子的情况下(即 nums[1:]nums[1:]),最大金额是 p_1;
在不偷窃最后一个房子的情况下(即 nums[:n-1]nums[:n−1]),最大金额是 p_2
综合偷窃最大金额: 为以上两种情况的较大值,即 max(p1,p2)max(p1,p2) 。
动态规划 模仿198 的标准答案写的
class Solution {
public int rob(int[] nums) {
if(nums.length==0) return 0;
if(nums.length==1) return nums[0];
int[] dp=new int[nums.length+1];
return Math.max( helper(Arrays.copyOfRange(nums, 0, nums.length - 1)),
helper( Arrays.copyOfRange(nums, 1,nums.length)) );
}
public int helper(int[] nums){
int[] dp=new int[nums.length+1];//0被当做第0个房子 不存在
dp[0]=0;
dp[1]=nums[0];
for(int i=2;i<=nums.length;i++){
//为什么这样呢
dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i-1]);
}
return dp[nums.length];//第0个房子不存在所以不算
}
}
取数组
Arrays.copyOfRange( nums, 0, nums.length-1);
lee337
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 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.
思路 递归 首先要看清题 其实就是二叉树结构 不能相邻节点 就是父节点选中 不能有子节点,可以选孙子节点
在解法1和解法2中 我们使用爷爷-两个孩子-4个孙子来说明问题
首先来定义这个问题的状态
爷爷节点获取到最大的偷取的钱数呢
首先要明确相邻的节点不能偷,也就是爷爷选择偷,儿子就不能偷了,但是孙子可以偷
二叉树只有左右两个孩子,一个爷爷最多2个儿子,4个孙子
根据以上条件,我们可以得出单个节点的钱该怎么算
4个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构
由于是二叉树,这里可以选择计算所有子节点
4个孙子投的钱加上爷爷的钱如下
int method1 = root.val + rob(root.left.left) + rob(root.left.right) + rob(root.right.left) + rob(root.right.right)
两个儿子偷的钱如下
int method2 = rob(root.left) + rob(root.right);
挑选一个钱数多的方案则
int result = Math.max(method1, method2);
public int rob(TreeNode root) {
if (root == null) return 0;
int money = root.val;
if (root.left != null) {
money += (rob(root.left.left) + rob(root.left.right));
}
if (root.right != null) {
money += (rob(root.right.left) + rob(root.right.right));
}
return Math.max(money, rob(root.left) + rob(root.right));
}
这样效率低,想到memo[ ] 存储
我们这一步针对重复子问题进行优化,我们在做斐波那契数列时,使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。
由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode当做key,能偷的钱当做value
修改一下上面的代码可得:
class Solution {
HashMap<TreeNode, Integer> map = new HashMap<>();
public int rob(TreeNode root) {
if(root==null) return 0;
// map.put(root,0);
if(map.containsKey(root)) return map.get(root);
int money=root.val;
if(root.left!=null){
money+=(rob(root.left.left)+rob(root.left.right));
}
if(root.right!=null){
money+=(rob(root.right.left)+rob(root.right.right));
}
int res= Math.max(money, rob(root.left)+rob(root.right));
map.put(root,res);
return res;
}
}
一般人想不到的终极解法3
上面两种解法用到了孙子节点,计算爷爷节点能偷的钱还要同时去计算孙子节点投的钱,虽然有了记忆化,但是还是有性能损耗。
我们换一种办法来定义此问题
每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷
当前节点选择偷时,那么两个孩子节点就不能选择偷了
当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)
我们使用一个大小为2的数组来表示 int[] res = new int[2] 0代表不偷,1代表偷
任何一个节点能偷到的最大钱的状态可以定义为
当前节点选择不偷: 当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
当前节点选择偷: 当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
表示为公式如下
root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) + Math.max(rob(root.right)[0], rob(root.right)[1])
root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
代码如下:
public int rob(TreeNode root) {
int[] result = robInternal(root);
return Math.max(result[0], result[1]);
}
public int[] robInternal(TreeNode root) {
if (root == null) return new int[2];
int[] result = new int[2];
int[] left = robInternal(root.left);
int[] right = robInternal(root.right);
result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
result[1] = left[0] + right[0] + root.val;
return result;
}