前端刷题路-Day85:打家劫舍 II(题号213)

343 阅读1分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

打家劫舍 II(题号213)

题目

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

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

示例 1:

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

示例 2:

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

示例 3:

输入:nums = [0]
输出:0

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

链接

leetcode-cn.com/problems/ho…

解释

这题啊,这题是梅开二度。

这题的题干和第一题没啥区别,主要是房子绕成了一圈,也就是第一个房子和最后一个房子是连在一起的,所以选了第一个就不能选最后一个,选了最后一个就不能选第一个。

笔者认为这里主要需要区分出是否选择了第一个,其它的和第一个保持一致就好了。

所以如果要对这题使用DP,需要从第一题的一维数组进化到二维数组,用数组的第一排元素表示选择的了第二个房子,第二排元素表示选择了第一个房子。

之后就是对这个DP二维数组进行循环更新操作,不过有两点特殊情况需要注意:

  • 初始化赋值阶段

    • 如果是第一排元素,证明此时没有选中第一个房子,所以dp[0][0]就是0了,而第二个元素是第二个元素,但第二个元素有可能不存在,因为数组的最小长度为1,所以如果不存在,需要将dp[1][1]赋值为Number.MAX_SAFE_INTEGER
    • 如果是第二排元素,证明此时选中了第一个房子,那么dp[1][0]需要选择第一个元素,dp[1][1]需要选中前两个元素中的最大值,但第二个元素有可能是不存在的,那么此时,取到的值就是NaN了,此时进行比较会遇到和第一题一样的问题,有可能是NaN,那么此时就需要在第二个元素不存在是将其赋值为Number.MAX_SAFE_INTEGER,和第一种情况一样。
  • 迭代阶段

    这里也需要根据是否选中第一个元素区别对待。

    整体迭代方式是一样的,就是根据前两个元素来推断出当前位置的最大值,这一点和上一题一样,不多赘述,重点是在于选中第一个元素的情况,如果此时选中了第一种情况,那么在迭代的最后一个位置,就不能再迭代了,因为最后一个元素不能选择,所以此时应该将上一次迭代的结果赋值给当前位置即可。

解决了这两个问题剩下就没什么困难的了,整体逻辑和上一题保持一致,需要处理细节问题。

自己的答案(DP+数组)

这就是解释中的答案,Easy。

var rob = function(nums) {
  const len = nums.length
  const dp = Array.from({length: 2}, () => new Array(len))
  dp[0][0] = 0
  dp[0][1] = nums[1] || Number.MIN_SAFE_INTEGER
  dp[1][0] = nums[0]
  dp[1][1] = Math.max(nums[1] || Number.MIN_SAFE_INTEGER, nums[0])
  for (let i = 2; i < len; i++) {
    if (i !== len - 1) {
      dp[1][i] = Math.max(dp[1][i - 2] + nums[i], dp[1][i - 1])
    } else {
      dp[1][i] = dp[1][i - 1]
    }
    dp[0][i] = Math.max(dp[0][i - 2] + nums[i], dp[0][i - 1])        
  }
  return Math.max(dp[0][len - 1], dp[1][len - 1])
}

将DP数组从一维变成了二维后,需要初始化赋值的元素自然也会多些,代码除了看上去有点恶心,逻辑有点难以理解之外,别的还好。

自己的答案(DP+降维)

有DP数组方案的答案,自然也会有DP降维的答案,降维这里和上一题也十分类似,只是需要从上一题的两个变量升级为4个变量,需要注意的地方也👆的解法一样,初始化赋值的问题,和选中第一个元素的最后迭代值的问题。

var rob = function(nums) {
  const len = nums.length
  let before00 = 0
  let before01 = nums[1] || Number.MIN_SAFE_INTEGER
  let before10 = nums[0]
  let before11 = Math.max(nums[1] || Number.MIN_SAFE_INTEGER, nums[0])
  for (let i = 2; i < len; i++) {
    if (i !== len - 1) {
      [before10, before11] = [before11, Math.max(before10 + nums[i], before11)]
    }
    [before00, before01] = [before01, Math.max(before00 + nums[i], before01)]
  }
  return Math.max(before01, before11)
}

代码看起来比二维数组的方案简单一些,但逻辑没有本质上的区别,单纯是因为变量去掉了一些特殊情况的判断。

更好的方法(DP+分组)

其实关于这题,是不是可以拆解为两个第一题?

  • 如果第一个房子可以选中

    那么该问题的区别是不是就变成了从数组的第一个元素到数组的倒数第二个元素?

  • 如果第一个房子不可以选中

    那么该问题的区间是不是就变成了从数组的第二个元素到数组的倒数第一个元素?

弄清楚这两个问题后答案其实也就呼之欲出了,只要写一个工具方法,用来计算某个区间内的可选最大值,分别对这两个区间进行计算操作,对比二者的结果,选择最大值,那么这题是不是就解决了?

这种解法虽然扫了两次数组,但在理解上面比直接强行解决的方案好了很多,不会出现多种情况的复杂判断逻辑,因为在一开始就已经对不同的情况进行了分组处理。

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

const robRange = (nums, start, end) => {
  let first = nums[start], second = Math.max(nums[start], nums[start + 1]);
  for (let i = start + 2; i <= end; i++) {
    const temp = second;
    second = Math.max(first + nums[i], second);
    first = temp;
  }
  return second;
}


PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)