第九章 动态规划 part09 198. 打家劫舍 213. 打家劫舍II 337. 打家劫舍III

第四十八天| 第九章 动态规划 part09 198. 打家劫舍 213. 打家劫舍II 337. 打家劫舍III

一、198. 打家劫舍

  • 题目链接:https://leetcode.cn/problems/house-robber/

  • 题目介绍:

    • 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

      给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

      示例 1:

      输入:[1,2,3,1]
      输出:4
      解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
           偷窃到的最高金额 = 1 + 3 = 4 。
      
  • 思路:

    • DP五部曲:

      • (1)确定dp数组及下标含义:

        • dp[i]:表示的是考虑到下标i(考虑下标i以及下标i之前),能偷到的最高金额
          
      • (2)确定递推公式:

        • 偷i:

          • 如果偷i的话,那么i-1就不能能偷,此时的最高金额就是dp[i-2] + nums[i];
            
        • 不偷i:

          • 如果不偷i,那么要考虑到i-1,不管i-1偷还是不偷,此时的最高金额就是dp[i-1];
            
      • (3)初始化dp数组:

        • dp[0]:只有一个0的时候,肯定是要偷0的,所以dp[0] = nums[0];
          dp[1]:有0和1两个的时候,就要考虑谁大就偷谁,所以dp[1] = Math.max(nums[0], nums[1]); 
          
      • (4)确定遍历顺序:

        • 从递推公式可以看出,dp[i]取决于dp[i-1]和dp[i-2],因此应该是正序遍历
          
  • 代码:

class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0]; 
        // 1. 确定dp数组及下标含义:
        // dp[i]:表示的是考虑到下标i(考虑下标i以及下标i之前),能偷到的最高金额
        int[] dp = new int[nums.length];
        // 3. 初始化dp数组
        // dp[0]:只有一个0的时候,肯定是要偷0的,所以dp[0] = nums[0];
        // dp[1]:有0和1两个的时候,就要考虑谁大就偷谁,所以dp[1] = Math.max(nums[0], nums[1]); 
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        // 4. 确定遍历顺序:
        // 从递推公式可以看出,dp[i]取决于dp[i-1]和dp[i-2],因此应该是正序遍历
        for (int i = 2; i < nums.length; i++) {
            // 2. 确定递推公式:
            //  2.1 偷i:如果偷i的话,那么i-1就不能能偷,此时的最高金额就是dp[i-2] + nums[i];
            //  2.2 不偷i:如果不偷i,那么要考虑到i-1,不管i-1偷还是不偷,此时的最高金额就是dp[i-1];
            dp[i] = Math.max((dp[i - 2] + nums[i]), dp[i - 1]);
        }
        return dp[nums.length - 1];
    }
}

二、213. 打家劫舍II

  • 题目链接:https://leetcode.cn/problems/house-robber-ii/

  • 题目介绍:

    • 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

      给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

      示例 1:

      输入:nums = [2,3,2]
      输出:3
      解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
      
  • 思路:

    • 这道题目和198. 打家劫舍是差不多的,唯一区别就是成环了
    • 对于一个数组,成环的话主要有如下三种情况:
      • 情况一:考虑不包含首尾元素
        • 第九章 动态规划 part09 198. 打家劫舍 213. 打家劫舍II 337. 打家劫舍III_第1张图片
      • 情况二:考虑包含首元素,不包含尾元素
        • 第九章 动态规划 part09 198. 打家劫舍 213. 打家劫舍II 337. 打家劫舍III_第2张图片
      • 情况三:考虑包含尾元素,不包含首元素
        • 第九章 动态规划 part09 198. 打家劫舍 213. 打家劫舍II 337. 打家劫舍III_第3张图片
    • 其实,情况二和情况三包含了情况一,因为我们定义的dp数组的含义是考虑到下标i(考虑下标i及下标i之前的全部),是考虑下标i而不是下标i一定会偷。
    • 所以本题就简化为:
      • 将情况二和三的数组带入到打家劫舍I的函数中,求两者的最大值即可
  • 代码:

class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        return Math.max(robAction(nums, 0, nums.length - 2), robAction(nums, 1, nums.length - 1));
    }

    public int robAction(int[] nums, int start, int end) {
        if (start == end) return nums[start];
        int[] dp = new int[nums.length];
        dp[start] = nums[start];
        dp[start + 1] = Math.max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            dp[i] = Math.max((dp[i - 2] + nums[i]), dp[i - 1]);
        }
        return dp[end];
    }
}

三、337. 打家劫舍III==(树形DP,难难难难难难难难难难难难难难难难)==

  • 题目链接:https://leetcode.cn/problems/house-robber-iii/

  • 题目介绍:

    • 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

      除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

      给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

      示例 1:

      第九章 动态规划 part09 198. 打家劫舍 213. 打家劫舍II 337. 打家劫舍III_第4张图片

      输入: root = [3,2,3,null,3,null,1]
      输出: 7 
      解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
      
  • 思路:

    • 这题思路太难想到了,考点是二叉树的遍历和DP的杂糅

    • 按照二叉树的递归三部曲,中间穿插DP五部曲来讲解:

      • 递归三部曲一、确定递归函数的参数和返回值:

        • (1)参数是二叉树的节点
        • (2)返回值是dp数组
          • (2.1)dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
          • (2.2)为什么一个二维数组就可以标记每个节点的状态呢?
            • 这是因为采用的是递归,递归返回的是dp数组,即记录了每个节点的状态。我们只要每遍历root的时候,创建一个dp数组,就能保证dp数组记录的是该节点的状态
      • 递归三部曲二、确定终止条件:

        • 遇到null就返回全0的dp
      • 递归三部曲三、确定单层递归的逻辑:

        • 根节点是否偷,取决于左右子节点的状态,因此这里需要使用后序遍历

          • 中:

            • 不偷:

              • Max(左孩子不偷,左孩子偷) + Max(右孩子不偷,右孩子偷)
                即:
                     dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
                
            • 偷:

              • 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
                即:
                	dp[1] = root.val + left[0] + right[0];
                
  • 代码:

class Solution {
    public int rob(TreeNode root) {
        int[] dp = traversal(root);
        return Math.max(dp[0], dp[1]);
    }

    // 递归三部曲一、确定递归函数的参数和返回值:
    // 1. 参数是二叉树的节点
    // 2. 返回值是dp数组
    //   2.1 dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
    //  2.2 为什么一个二维数组就可以标记每个节点的状态呢,这是因为下面采用的是递归,递归返回的是dp数组,即记录了每个节点的状态。
    public int[] traversal(TreeNode root) {
        // 递归三部曲二、确定终止条件
        int[] dp = new int[2];
        if (root == null) return dp;
        // 递归三部曲三、确定单层递归的逻辑
        //   根节点是否偷,取决于左右子节点的状态,因此这里需要使用后序遍历
        // 左
        int[] left = traversal(root.left);
        // 右
        int[] right = traversal(root.right);
        // 中
        // dp数组中的第一位表示该节点不偷
        dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        // dp数组中的第二位表示该节点偷
        dp[1] = root.val + left[0] + right[0];
        return dp;
    }
}

你可能感兴趣的:(动态规划,算法)