代码随想录算法训练营第四十七天 | 198. 打家劫舍、213. 打家劫舍 II、337. 打家劫舍 III

25 阅读3分钟

198. 打家劫舍

链接

文章链接

题目链接

第一想法

这道题怎么说呢,我认为代码有点问题,但是居然能通过测试...所以我这里我先写我的思路

  1. 首先是dp数组的含义 dp[i]为第i天最大的偷窃数为dp[i]
  2. 初始化很简单,因为没偷窃时都是0,但是依据递推公式,所以必须先初始dp[0]=nums[0], dp[1]=Math.max(nums[0], nums[1])
  3. 递推公式为dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]),这里我理解的为不抢劫第i天,所以最大金额为dp[i-1](抢第i-1天),如果抢第i天,则i-1天不能抢所以只能拿dp[i-2]算
  4. 举例[1,1,4,1,1,9]结果为[1,1,5,5,6,14]
function rob(nums: number[]): number {
  if (nums.length == 1) return nums[0]
  let dp: number[] = new Array(nums.length).fill(0)
  dp[0] = nums[0]
  dp[1] = Math.max(nums[0], nums[1])
  for (let i = 2; i < nums.length; i++) {
    dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i])
  }
  return dp[dp.length - 1]
}

虽然这道题写出来了,但是关于递推公式还是有一点困惑的拿dp[i-2]算难道dp[i-2]一定偷吗,就是我举个那个例子,但是居然通过了。

看完文章后的想法

果然,虽然我写对了,但是思路是有大问题的,首先是dp[i]的含义dp[i]为包含当前i天时能偷取的最大金额。其次,就是递推公式的理解,首先是不抢第i天,则金额为dp[i-1](这里和抢不抢i-1天没关系),如果抢第i天的话,则一定一定不能拿i-1算,如果拿i-1算可能i-1天也被抢了。所以那dp[i-2]天,因为不管抢不抢i-2天都可以拿i-2算。下面来个图例:

image.png

思考

这道题虽然写出来了,但是思想有问题,陷入自己的陷阱当中了,看完文章后就发现自己的问题所在了。这道题的难点在于dp数组的理解以及递推公式。只要把这两个搞清楚这道题就不算太难了。

213. 打家劫舍 II

链接

文章链接

题目链接

第一想法

emmm,刚刚做了打家劫舍,所以看到这个就和原来就一个不同,就是首和尾相连,所以我的想法是搞一个辅助数组,判断最后一个元素-2天是否使用了第一个元素,使用了则返回dp[dp.length-1],详细解释在代码随想录中展示

//这个代码是无法通过用例的
function rob(nums: number[]): number {
  if (nums.length == 1) return nums[0]
  let dp: number[] = new Array(nums.length).fill(0)//dp数组
  let Boo: boolean[] = new Array(nums.length).fill(false)//辅助函数 判断是否使用了第一个元素
  dp[0] = nums[0]
  dp[1] = Math.max(nums[0], nums[1])
  Boo[0] = true//初始化
  Boo[1] = nums[0] > nums[1]//初始化 如果不偷下标为1的房子 则说明需要那0天算 所以也要设置为true
  for (let i = 2; i < nums.length; i++) {
    if (i == nums.length - 1) {
      if (dp[i - 1] >= dp[i - 2] + nums[i] || Boo[i - 2]) return Math.max(dp[i - 1], nums[i])
      else return dp[i - 2] + nums[i] //可以偷最后一天
    }
    dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i])
    Boo[i] = dp[i - 1] > dp[i - 2] + nums[i] ? Boo[i - 1] : Boo[i - 2]//判断偷i天后是否是偷了第0天
  }
  return dp[dp.length - 1]
}

看完文章后的想法

emmmm....还是一样,自己把考虑房子和偷房子概念搞混了,

考虑当前的房子不一定偷当前的房子!!!!

考虑当前的房子不一定偷当前的房子!!!!

考虑当前的房子不一定偷当前的房子!!!!

所以我的那种想法是完全错误的,依照题目,最后一天和第一天不能同时考虑,如果同时考虑的话就有可能两个同时被偷,所以要分开考虑第一天和最后一天,如图:

考虑第一天,则不考虑最后一天

image.png 考虑最后一天则不考虑第一天:

image.png 代码如下

function rob(nums: number[]): number {
  const foo = (start: number, end: number, nums: number[]) => {//foo就是初级版的打家劫舍 start和end用来控制起始位置和终止位置
    if (nums.length == 1) return nums[0]
    let dp: number[] = new 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 - 1], dp[i - 2] + nums[i])
    }
    return dp[end - 1]
  }
  //通过比较两种情况的最大值来确定最高偷取金额
  return Math.max(foo(0, nums.length - 1, nums), foo(1, nums.length, nums))
}

思考

还是没考虑清楚考虑房子和偷窃房子,只要考虑清楚,就没啥问题了。同时这道题因为首尾是相连的,所以需要分开考虑这两种情况,自己是没想到的,需要收藏回看。

337. 打家劫舍 III

链接

文章链接

题目链接

第一想法

emmm...考虑的问题少了。我看到事例我就想到了如果这一层的某个元素选了,那么这一层的元素都选上肯定是最大的,因为当前节点选了,说明上一层的父节点不能选,结果就按照这个思路写了,发现报错....因为我没考虑这种情况:

image.png

这是情况最大值是7而不是6,所以即使是在相邻的两层也是可以选的,只要不是直接相连的就可以,代码如下

//下面代码是无法通过全部测试用例得到
function rob(root: TreeNode | null): number {
  let dp: number[] = []
  let curr: TreeNode[] = []
  if (root) curr.push(root)
  while (curr.length > 0) {
    let size: number = curr.length
    let sum: number = 0
    dp.push(0)
    while (size--) {
      let node: TreeNode = curr.shift()!
      sum += node.val
      if (node.left) curr.push(node.left)
      if (node.right) curr.push(node.right)
    }
    if (dp.length == 1) dp[0] = sum
    else if (dp.length == 2) dp[1] = Math.max(dp[0], sum)
    else {
      dp[dp.length - 1] = Math.max(dp[dp.length - 2], dp[dp.length - 3] + sum)
    }
  }
  return dp[dp.length - 1]
}

看完文章后的想法

果然,文章还是想的全面,这道题是跟树结合起来要考的,而且它的递归函数比较奇怪,就两个dp[0]是存放不偷当前的价值,dp[1]是存放偷当前的房子之后的价值,而且这道题运用到后序遍历的知识,因为返回值为dp,所以代码如下:

function rob(root: TreeNode | null): number {
  const foo: (root: TreeNode | null) => number[] = (root) => {
    if (root == null) return [0, 0]//dp数组 遇到null说明是最底层了
    let left: number[] = foo(root.left)//左子树的结果
    let right: number[] = foo(root.right)//右子树的结果
    //偷当前节点
    let val1: number = left[0] + right[0] + root.val
    //不偷当前节点
    let val2: number = Math.max(left[0], left[1]) + Math.max(right[0], right[1])
    return [val2, val1]
  }
  return Math.max(...foo(root))
}

举例:

image.png

思考

这道题其实是不难的,但是我确实没想到,这是第一次遇到数的动态规划,第一次遇到这种思想

今日总结

今天耗时2.5小时,今天比较失败,三道题就会最简单的一道,而且打家劫舍的思想我没有想清楚,考虑房子和偷房子是两个概念。今天的第三道题是值得纪念的一道题,第一次遇到动态规划和树结合,思想值得学习。