Day50~198.打家劫舍、213.打家劫舍II、337.打家劫舍III

92 阅读4分钟

摘要

本文主要介绍了LeetCode动态规划的几个题目,包括198.打家劫舍、213.打家劫舍II、337.打家劫舍III。

1、198.打家劫舍

1.1 思路

动规五部曲

  • dp 数组以及下标的含义

    • dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
  • 递推公式

    • 决定dp[i]的因素就是第i房间偷还是不偷

      • 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i]
      • 如果不偷第i房间,那么dp[i] = dp[i - 1]
    • 然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

  • dp 数组如何初始化

    • 从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

      • dp[0] 一定是 nums[0]
      • dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
  • dp 数组遍历顺序

    • dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
  • 举例推导dp数组

1.2 代码

    public int rob(int[] nums) {
        int len = nums.length;
        int[] dp = new int[len];
        dp[0] = nums[0];
​
        for(int i=1; i<len; i++) {
            if(i == 1) {
                dp[i] = Math.max(nums[0], nums[1]);
                continue;
            }
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }
        return dp[len-1];
    }

2、213.打家劫舍II

2.1 思路

  • 思路

    • 对于一个数组,成环的话主要有如下两种情况:

      • 情况一:考虑包含首元素,不包含尾元素

      • 情况二:考虑包含尾元素,不包含首元素

    • 分析到这里,本题其实比较简单了。 剩下的和198.打家劫舍就是一样的了

  • 注意事项

    • dp 数组初始化大小仍然为 nums.length 而不是 end-start,以避免出现空指针异常

      • int[] dp = new int[nums.length];

2.2 代码

    public int rob(int[] nums) {
        int len = nums.length;
        if(len == 1) {
            return nums[0];
        }
​
        return Math.max(doRob(nums, 0, len - 1), doRob(nums, 1, len));
    }
​
    // 左闭右开
    public int doRob(int[] nums, int start, int end) {
        int len = end - start;
        int[] dp = new int[nums.length];
        dp[start] = nums[start];
​
        for(int i=start+1; i<end; i++) {
            if(i == start+1) {    
                dp[i] = Math.max(nums[start], nums[start+1]);
                continue;
            }
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }
        return dp[end-1];
    }

3、337.打家劫舍III

3.1 思路

  • 思路

    • 本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算
    • 与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。
    • 如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”
  • 动态规划

    • 动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱
  • 递归

    • 确定递归函数的参数和返回值

      • 其实这里的返回数组就是dp数组,所以dp数组(dp table)以及下标的含义:

        • 下标为0记录不偷该节点所得到的的最大金钱
        • 下标为1记录偷该节点所得到的的最大金钱。
    • 确定终止条件

      • 在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回。 这也相当于dp数组的初始化 。
    • 确定遍历顺序

      • 首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。

        • 通过递归左节点,得到左节点偷与不偷的金钱。
        • 通过递归右节点,得到右节点偷与不偷的金钱
    • 单层递归逻辑

      • 如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的

        • val0 = max(left[0], left[1]) + max(right[0], right[1]);
      • 如果是偷当前节点,那么左右孩子就不能偷

        • val1 = cur->val + left[0] + right[0];
      • 最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

3.2 代码

    public int rob(TreeNode root) {
        int[] arr = doRob(root);
        return Math.max(arr[0], arr[1]);
    }
​
    public int[] doRob(TreeNode root) {
        if(root == null) {
            return new int[]{0, 0};
        }
​
        int[] left = doRob(root.left);
        int[] right = doRob(root.right);
​
        int val0 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        int val1 = root.val + left[0] + right[0];
        return new int[]{val0, val1};
    }

参考资料

代码随想录-198.打家劫舍

代码随想录-213.打家劫舍II

代码随想录-337.打家劫舍III