xxleyi / loop_invariants

记录以及整理「循环不变式」视角下的算法题解

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

leetcode 198. 打家劫舍

xxleyi opened this issue · comments

题:

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

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

示例 1:

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

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
  偷窃到的最高金额 = 2 + 9 + 1 = 12 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


解:

这个题有一个切入点:第 i 个元素只有抢或不抢两种选择。

那么什么时候应该抢,什么时候不应该抢呢?

如果把当前元素抢了,能够让最大金额增加,是不是应该抢一下?

但这并不是必然,因为暂时不抢当前元素,后续可能会有更大收益。

那如果不抢当前元素,会造成什么后果呢?

不抢的话,相当于重新开始,在之前能抢到的最大金额基础上重新开始。

到这里,我们应该隐约意识到,我们必须维护至少两个「循环不变式」相关的变量:迄今为止最大,以及迄今为止第二大。

如果当前元素与迄今为止第二大元素相加大于迄今为止最大,则取而代之。旧的最大就是新的迄今为止第二大。
否则就不抢,而且不抢的话,迄今为止第二大和旧的最大保持一致(相当于重新开始)。

循环之前初始化「循环不变式」相关的变量:secondLargest = largest = 0

循环过程中更新变量以确保「循环不变式」的依据:抢不抢当前元素 e

  • secondLargest + e > largest: [secondLargest, largest] = [largest, secondLargest + e]
  • 否则:secondLargest = largest = largest,即回到开始状态

直到循环终止,循环不变式始终得到正确维护,答案为 largest

var rob = function(nums) {
  // initialize loop invariant related variables before loop:
  let secondLargest = largest = 0

  for (let e of nums)
    // update secondLargest and largest: choose or not choose the current e
    // ensure our loop invariant 
    [secondLargest, largest] = [largest, Math.max(secondLargest + e, largest)]
  
  // termination: largest after the loop is our answer
  return largest
};

这个题目,我之前尝试写能被接受的递归,一直没写出来,站在「循环不变式」的视角下,自然而然就出来了:

var rob = function(nums) {
  function loop(i=-1, secondLargest=0, largest=0) {
    if (i === nums.length - 1) return largest
    return loop(i+1, largest, Math.max(largest, nums[i+1] + secondLargest))
  }
  return loop()
}

是的,这看上去似乎纯属多此一举,这个递归函数和循环根本没有区别好不好。嗯,我也意识到这一点了,我直接用 loop 命名了我的递归函数。

但我想说,这其中有大微妙:递归版的实现是纯函数,没有任何 state 的修改。

这其中涉及的东西还挺多的,此处略过不表。