算法日记-03打家劫舍系列问题总结

打家劫舍系列问题总结

力扣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 。
思路分析

分析题意可知,首先我们需要明确一点,我们偷取金额必须间隔着取不能连续取

因此我们需要考虑到如果偷取第i个房间 那么我们能获取的最大收益是多少

所以到此经过我们分析可以定义出dp数组 **dp[i]**表示我们偷取0-i个房间所能获取到的最大收益

接着我们需要考虑dp数组的初始化问题 这里我们可以通过分析给出的示例来分析

**dp[0]**表示我们偷取数组下标为0的房间能获取的最大金额

dp[0] = nums[0]

**dp[1]**当我们偷取1号房间时因为不能连续偷取 所以我们只能选择偷取0号房间或者1号房间

dp[1] = Math.max(nums[0],nums[1])

同理我们分析dp[2] 当我们选择偷2号房间时 那么我们就无法偷取一号房间

当我们不偷取2号房间时,那么我们能偷取到的最大金额为dp[1] 不难得出以下关系式

dp[2] = Math.max(nums[0]+nums[2],dp[1]);
//nums[0]+nums[2]分析该表达式 我们可知由于只能间隔偷 所以当我们偷取i号房间时 我们还能偷到的金额就是dp[i-2]
//这时我们得到最终表达式
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
代码实现
class Solution {
    public int rob(int[] nums) {
        //定义dp数组 dp[i]表示从0到i所能抢到的最大金额
        int[] dp = new int[nums.length];
        //初始化dp数组 dp[0] = nums[0] dp[1] = Math.max(nums[0],nums[1])
        //dp[2] = dp[1]  dp[2-2]+nums[2]
        //dp[3] = dp[2]  dp[3-2]+nums[3]
        dp[0] = nums[0];
        if(nums.length==1){
            return nums[0];
        }
        dp[1] = Math.max(nums[0],nums[1]);
        for(int j=2;j<nums.length;j++){
            dp[j] = Math.max(dp[j-1],dp[j-2]+nums[j]);
        }
        return dp[nums.length-1];
    }
}
力扣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
思路分析

本题以上道题目最大的不同是,这次我们遇到环了。所以这道题目递推关系式和上题一致 本题就不过多阐述了。

那么我们该如何解决成环问题呢?因为成环所以数组的头和尾是相连着的,那么显而易见的当我们拿走头房间时,我们就不能拿走尾房间,反之我们拿走尾房间时,也无法拿走头房间。

所以最好想到的思路就是我们分为两种情况讨论。

方案一:当我们偷头房间,不偷尾房间

当我们偷取头房间时,那么我们则不需要考虑尾房间,因此遍历的范围从原来的0-nums.length-1变为0-nums.length-2

方案二:当我们偷尾房间,不偷头房间。

当我们偷取尾房间时,那么我们则不需要考虑头房间,因此遍历的范围从原来的0-nums.length-1变为1-nums.length-1

所以本题我们需要定义两个dp数组 dpdp1分别记录不同情况下我们所能获取的最大值

当然了初始化也是不同的方案一我们需要初始化dp[0]和dp[1],而方案二我们则需要初始化dp[1]和dp[2]

造成这样的原因是因为方案二中我们没有取0号房间的现金,所以1号房间就相当于方案一中的0号房间,具体的代码实现如下。

代码实现
class Solution {
    public int rob(int[] nums) {
        //定义dp数组 dp[i]表示从0-i房间能偷到的最大金额
        int[] dp = new int[nums.length];
        int[] dp1 = new int[nums.length];
        //初始化dp数组
        if(nums.length==0){
            return 0;
        }
        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++){
            if(i==nums.length-1){
                //说明偷最后一个房间了  那么我们就不能偷第一个房间
                dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]-nums[0]);
            }else{
                dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
            }
        }
        dp1[0] = 0;
        dp1[1] = nums[1];
        if(nums.length==2){
            return Math.max(nums[0],nums[1]);
        }
        dp1[2] = Math.max(nums[1],nums[2]);
        //不取第一个
        for(int i=3;i<nums.length;i++){
            if(i==nums.length-1){
                //说明偷最后一个房间了  那么我们就不能偷第一个房间
                dp1[i] = Math.max(dp1[i-1],dp1[i-2]+nums[i]);
            }else{
                dp1[i] = Math.max(dp1[i-1],dp1[i-2]+nums[i]);
            }
        }
        int result = Math.max(dp[nums.length-1],dp1[nums.length-1]);

        return result;
    }
}
力扣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
思路分析
方案一

通过分析题目我们可以知道本题不是在数组中进行遍历了,而是用到了二叉树。那么这时我们可以想到一点,如果我们取了一个节点的值,那么我们将无法再去拿走该节点的左右子节点的值了。也就是说我们每次只能拿走该节点或者该节点两个子节点的值。到这里我们就应该能想到,通过后序遍历左右中遍历二叉树,我们可以得到从根节点开始所能获取的最大值。

那么本题我们需要考虑的点就是当我们遍历到一个节点时,需要选择取或者不取,所以本题我们需要为每个节点定义一个dp数组 dp[2],其中我们用dp[0]表示不放入当前节点的最大值 dp[1]表示放入当前节点的最大值。那么我们该如何处理该节点的取与不取呢?我们分两种情况进行讨论,取出该节点,不取该节点。

取出该节点:

当我们取出当前节点node.val时,我们就不能取出他左右子节点的值了所以我们取出不去左右子节点的最大值

即左节点的dp[0]和右节点的dp[0],这也是为什么我们要采取后序遍历才解决该问题的原因之一,因为我们首先要知道子节点的情况才能计算出父节点的情况。得到以下表达式:

int left[] = dfs(node.left);
int right[] = dfs(node.right);
int val1 = node.val+left[0]+right[0];

所以我们得到后序遍历的解法如下:

    public int[] dfs(TreeNode node){
        //dp[0]表示不放入当前节点的最大值 dp[1]表示放入当前节点的最大值
        int[] dp = new int[2];
        if(node==null){
            return dp;
        }
        int left[] = dfs(node.left);
        int right[] = dfs(node.right);
        //如果放入当前节点 那么他的左右子节点就不能放入
        int val1 = node.val+left[0]+right[0];
        //如果不放入当前节点 那么就放入他左右子节点的最大值
        int val2 = Math.max(left[0],left[1])+Math.max(right[0],right[1]);
        dp[0] = val2;
        dp[1] = val1;
        return dp;
    }
方案二

当然本题我们也可以用暴力递归来解决,当然也是分为两种情况,那就是取当前节点和不取当前节点。

取当前节点

如果取出当前节点的话,那么我们就不能取出该节点的左右节点了,但是我们可以取出他该节点的左右节点的子节点,即:

        //取当前节点和当前节点子节点的孩子 root为根节点  rob(为递归函数)
        int val = root.val;
        if(root.left!=null){
            val += rob(root.left.left)+rob(root.left.right);
        }
        if(root.right!=null){
            val += rob(root.right.left)+rob(root.right.right);
        }
		//得到的val就是取出该节点所能拿到的最大值

不取当前节点

如果我们不取出当前节点的话,我们就可以取出当前节点左右子节点,即:

		//取出左右子节点
		int rVal = rob(root.right);//左节点的值
		int lVal = rob(root.left);//右节点的值
		int val = rVal + lVal;//不取当前节点的值

所以经以上分析我们可以得到本题的暴力解法如下:

    public int rob(TreeNode root) {
        //int[] dp = dfs(root);
        //int result = Math.max(dp[0],dp[1]);
        if(root==null){
            return 0;
        }
        //取当前节点和当前节点子节点的孩子
        int val = root.val;
        if(root.left!=null){
            val += rob(root.left.left)+rob(root.left.right);
        }
        if(root.right!=null){
            val += rob(root.right.left)+rob(root.right.right);
        }
        //这边比较取出当前节点的值是否大于不取当前节点的值
        int result = Math.max(val,rob(root.right)+rob(root.left));
        //此时val的值为取根节点和根节点的子节点
        return result;

但是本题采用暴力解法在力扣上会超时,原因是我们会重复遍历我们已经得到过结果的节点,因此可以在次代码的基础上用map集合记录下我们已经判断过最值的节点,这样在每次取相同的元素前我们先去map集合中查找,就不用重复遍历之前得到结果的节点了。

修改以后的代码如下:

代码实现

使用map集合记录最值的解法:

    //用一个map来记录遍历过的节点值
    Map<TreeNode,Integer> map = new HashMap<TreeNode,Integer>();
    public int rob(TreeNode root) {
        if(root==null){
            return 0;
        }
        if(map.containsKey(root)){
            return map.get(root);
        }
        //取当前节点和当前节点子节点的孩子
        int val = root.val;
        if(root.left!=null){
            val += rob(root.left.left)+rob(root.left.right);
        }
        if(root.right!=null){
            val += rob(root.right.left)+rob(root.right.right);
        }
        int result = Math.max(val,rob(root.right)+rob(root.left));
        map.put(root,result);
        //此时val的值为取根节点和根节点的子节点
        return result;
    }

使用后序遍历的解法:

    public int rob(TreeNode root) {
        int[] dp = dfs(root);
        int result = Math.max(dp[0],dp[1]);
    }

    public int[] dfs(TreeNode node){
        //dp[0]表示不放入当前节点的最大值 dp[1]表示放入当前节点的最大值
        int[] dp = new int[2];
        if(node==null){
            return dp;
        }
        int left[] = dfs(node.left);
        int right[] = dfs(node.right);
        //如果放入当前节点 那么他的左右子节点就不能放入
        int val1 = node.val+left[0]+right[0];
        //如果不放入当前节点 那么就放入他左右子节点的最大值
        int val2 = Math.max(left[0],left[1])+Math.max(right[0],right[1]);
        dp[0] = val2;
        dp[1] = val1;
        return dp;
    }
  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值