代码随想录-动态规划-打家劫舍

60 阅读5分钟

打家劫舍问题

T198-打家劫舍

见LeetCode第198题[打家劫舍]

题目描述

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

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

示例 1:

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

我的思路

  • 定义int[] dp表示在当前只有i个房间下,能够获得的最多的金额
  • 不能访问两个相邻的房间,因此状态转移方程为:
    • dp[i] = Math.max(dp[i - 2] + values[i], dp[i - 1])
  • 需要注意初始化条件:dp[0], dp[1]
public int rob(int[] nums) {
    if (nums.length == 1) return nums[0];
    if (nums.length == 2) return Math.max(nums[0], nums[1]);

    // 初始化 dp 数组,表示仅有 i 个房间下能够获取的最大金额
    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(
                nums[i] + dp[i - 2], // 偷
                dp[i - 1] // 不偷当前房间
        );
    }
    return dp[nums.length - 1];
}

计算复杂度分析

  • 时间复杂度:O(N)O(N)
  • 空间复杂度:O(N)O(N)

T213-打家劫舍II

见LeetCode第213题[打家劫舍II]

题目描述

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

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

示例 1

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

我的思路

  • 问题的关键在于,对于最后一间屋子,如何判断该不该偷?
  • 第一间房子和最后一间房子只能选择一个
  • 可以为每个偷窃过的房间设置一个标记,如果偷过了,就置为true
  • 可以遍历[0, n - 2][1, n - 1]两个子数组,两者取最大
public int rob(int[] nums) {
    // 基本情况罗列
    if (nums.length == 1) return nums[0];
    if (nums.length == 2) return Math.max(nums[0], nums[1]);
    if (nums.length == 3) return Math.max(Math.max(nums[0], nums[1]), nums[2]);

    // 选择偷第一个房间,那么房间队列相当于只有 0 ~ n-2
    int[] dpHead = new int[nums.length - 1];
    dpHead[0] = nums[0];
    dpHead[1] = Math.max(dpHead[0], nums[1]);
    for (int i = 2; i < nums.length - 1; i++) {
        dpHead[i] = Math.max(nums[i] + dpHead[i - 2], dpHead[i - 1]);
    }
    // 选择偷最后一个房间,那么房间队列相当于只有 1 ~ n - 1
    int[] dpTail = new int[nums.length - 1];
    dpTail[nums.length - 2] = nums[nums.length - 1];
    dpTail[nums.length - 3] = Math.max(dpTail[nums.length - 2], nums[nums.length - 2]);
    for (int i = nums.length - 4; i >= 0; i--) {
        dpTail[i] = Math.max(nums[i + 1] + dpTail[i + 2], dpTail[i + 1]);
    }
    return Math.max(dpTail[0], dpHead[nums.length - 2]);
}

计算复杂度分析

  • 时间复杂度:O(2N)O(N)O(2N) \approx O(N)
  • 空间复杂度:O(2N)O(N)O(2N) \approx O(N)

打家劫舍III

见LeetCode第337题[打家劫舍III]

题目描述

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

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

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

示例

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

我的思路

没思路

题解思路

  • 动态规划的三要素:状态状态转移方程dp数组定义

首先关注一下状态,对于某个节点,其有两种状态:

  • 选择该节点,则不能选择其所有的子节点
  • 不选择该节点,则其可以选择、亦可以不选择其子节点

状态是如何进行转移的呢?如何推导出状态转移方程呢?

对于当前节点root的某一个子节点leftleft也有两种状态,即选择节点最大值left[0]和不选择节点的最大值left[1]

那么,对于root的两种状态,我们可以分别给出状态的方程:

  • 选择则不能选择子节点:root.val + left[1] + right[1]
  • 不选择则子节点可选可不选:Math.max(left[0], left[1]) + Math.max(right[0], right[1])

最后,根据这两种状态,选择最大值即可。

因为我们需要获取左、右孩子的状态,才能对root节点进行决策,因此使用后序遍历。

public int rob(TreeNode root) {
    if (root == null) return 0;

    int[] opts = postOrder(root);

    return Math.max(opts[0], opts[1]); // 选择根节点还是不选择根节点
}

/**
 * 后序遍历求出根节点的两种状态最大值:
 * 选择 or 不选择
 * @param root
 * @return
 */
private int[] postOrder(TreeNode root) {
    if (root == null) return new int[]{0, 0};

    // 获取左右孩子两种状态的值:选择 | 不选择
    int[] leftOpts = postOrder(root.left);
    int[] rightOpts = postOrder(root.right);

    // 选择当前的节点,则孩子不能选择
    int opt1 = root.val + leftOpts[1] + rightOpts[1];
    // 不选择当前节点则,孩子可以选择,可以不选择
    int opt2 = Math.max(leftOpts[0], leftOpts[1]) + Math.max(rightOpts[0], rightOpts[1]);

    return new int[]{opt1, opt2};

}

计算复杂度分析

  • 时间复杂度:需要遍历每个节点,因此时间复杂度为O(N)O(N)
  • 空间复杂度:空间复杂度和栈的深度有关,最坏的情况下为链表,则需要O(N)O(N)的额外空间去保存每个节点的两个状态