Leetcode打家劫舍问题总结
Leetcode打家劫舍问题总结

Leetcode打家劫舍问题总结

打家劫舍 I

简单说就是相邻房间不能偷取,偷取下标为 i 的房间时,i – 1 的房间不能偷,只有 i – 2 (或 i + 2)的房间可以偷。

由于有这样的状态,自然而然想到了动规求解。

1. 确定 dp 数组的含义:

包含下标为 i 以及之前的房间偷窃的最大金额为 dp[i]

2. 确定递推公式:

遍历到下标为 i 的房间时,有两种状态:

  • 偷取 i ,则为上一个偷取房间的状态+房间 i 的金额 :dp[i] = dp[i – 2] + nums[i]
  • 不偷取 i ,说明 i – 1 已经偷取过,则直接返回上一个房间的状态:dp[i] = dp[i – 1]

综上,dp[i] = max(dp[i – 1], dp[i – 2] + nums[i])

3. 初始化:

  • 对于 dp[0],金额只能是 nums[0]
  • 对于 dp[1],金额为 nums[0] 和 nums[1] 中的最大值,这样才能选出最大的金额

4. 确定遍历顺序:

dp[i] 是根据dp[i – 2] 和 dp[i – 1] 推导出来的,那么一定是从前到后遍历

代码如下:

class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];

        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Math.max(dp[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];
    }
}


打家劫舍 II

这道题在上一题的基础上,变成了环形数组。除了上一题的条件,增加了首尾相邻的房间也不能偷取的条件。

这道题很容易掉进处理环形数组的陷阱里,其实分情况就可以了(借用代码随想录的图):

这里是考虑!并不是一定包含尾元素,比如情况三,取 1 和 3 就是最大的情况。

然后我们可以发现,情况二和情况三包含了情况一,所以只关注这两种情况就可以。

分析到这里,给出代码:

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

    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 - 1], dp[i - 2] + nums[i]);
        }
        return dp[end];
    }
}


打家劫舍 III

这道题算是树形dp的入门题目,因为是在树上进行状态转移

对于一个节点,选或不选的状态是由它的孩子节点决定的,这显然会提醒我们使用后序遍历。

如下图所示,从底层的叶子节点开始,每一个节点都分为了 选/不选 的两种状态值,那么向上看,他们的父节点也具有 选/不选 的两种状态值,而父节点的状态值显然是根据孩子节点得出的,这是更直观的理解。

以根节点 2 为例:

  • 如果要选取 2 ,那么他的两个孩子节点就不能选取,只选取根节点的节点值
  • 如果不选取 2 ,那么就要选取两个孩子节点各自最大状态值的和

这里可以使用一个长度为2的数组,记录每个节点偷与不偷所得到的的最大金钱。下标0,我们认定为选取当前节点得到的金额;下标1,我们认定为不选取当前节点得到的金额。

代码如下:

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

    // 长度为2的数组,0:偷,1:不偷
    public int[] robAction(TreeNode node) {
        if (node == null) { //递归边界
            // 空节点,没有孩子
            return new int[]{0, 0};
        }
        int[] left = robAction(node.left);  // 递归左子树
        int[] right = robAction(node.right);    // 递归右子树
        // 选此节点 则不选叶子节点
        int rob = left[1] + right[1] + node.val;
        // 不选此节点,选叶子节点
        int notRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        return new int[]{rob, notRob};
    }
}


总结

劫舍系列简单来说就是:数组上连续元素二选一,成环之后连续元素二选一,在树上连续元素二选一,所能得到的最大价值

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

index