leetcode刷题笔记——【动态规划】打家劫舍问题

167 阅读4分钟

代码随想录 (programmercarl.com)

1. 基础打家劫舍问题

  1. dp数组的含义:dp[i]表示 考虑包括下标为i的房屋,最多可以偷盗的金额

  2. 递推公式:考虑两种情况,一种是当前房屋偷盗,则是dp[i-2] + nums[i], 另一种情况是当前房屋不偷,则考虑dp[i-1](可能偷可能不偷), 最终递推公式为 dp[i] = max((dp[i-2] + nums[i]), dp[i-1])

  3. 遍历顺序:从前到后

  4. 初始化:dp[0] = nums[0], dp[1] = max(nums[0], nums[1])

  5. dp数组举例

2. 成环的打家劫舍问题

题目: 213. 打家劫舍 II

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

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

教程 : 代码随想录 (programmercarl.com)

考虑三种情况,1. 考虑不包含首尾两个元素,2. 考虑不包含头元素,包含尾元素,3.考虑包含头元素,不包含尾元素。

注意我这里用的是"考虑" ,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素!

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了

代码

var rob = function(nums) {
  const n = nums.length
  if (n === 0) return 0
  if (n === 1) return nums[0]
  const result1 = robRange(nums, 0, n - 2)
  const result2 = robRange(nums, 1, n - 1)
  return Math.max(result1, result2)
};

const robRange = (nums, start, end) => {
  if (end === start) return nums[start]
  const dp = Array(nums.length).fill(0)
  dp[start] = nums[start]
  dp[start + 1] = Math.max(nums[start], nums[start + 1])
  for (let i = start + 2; i <= end; i++) {
    dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1])
  }
  return dp[end]
}

3. 树形打家劫舍问题

代码随想录 (programmercarl.com)

1. 题目

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

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

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

2. 分析

树形打家劫舍问题是将递归问题和动态规划结合在了一起,因此要同时考虑递归的步骤和动态规划的步骤。

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

    这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。其实这里的返回数组就是dp数组。

    所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱

  2. 确定终止条件

    在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

    这也相当于dp数组的初始化

  3. 确定遍历顺序

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

    通过递归左节点,得到左节点偷与不偷的金钱。

    通过递归右节点,得到右节点偷与不偷的金钱。

  4. 确定单层递归的逻辑

    如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义

    如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

    最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

  5. 举例推导dp数组

image.png

3. 代码

const rob = root => {
    // 后序遍历函数
    const postOrder = node => {
        // 递归出口
        if (!node) return [0, 0];
        // 遍历左子树
        const left = postOrder(node.left);
        // 遍历右子树
        const right = postOrder(node.right);
        // 不偷当前节点,左右子节点都可以偷或不偷,取最大值
        const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        // 偷当前节点,左右子节点只能不偷
        const Do = node.val + left[0] + right[0];
        // [不偷,偷]
        return [DoNot, Do];
    };
    const res = postOrder(root);
    // 返回最大值
    return Math.max(...res);
};