198.打家劫舍 - 从分治到动态规划

452 阅读4分钟

本文讲解 198. 打家劫舍 的解法

题目

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

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

 

示例 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

提示:

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

解法 1: 分治

根据题意,要获得偷 0~i 间房子的最大金额,并且不能连续偷两个房子

对于第 i 个房间,可以选择偷或者不偷,将其分解成两种情况:

  1. 偷第 i 间房子能获得的最大金额 f(i,偷)
  2. 不偷第 i 间房子能获得的最大金额 f(i,不偷)

取其中的较大者,即为偷 0~i 间房子的最大金额

对于第一种情况,因为我们偷了第 i 间房子,所以不能偷第 i-1 间房子,这种情况能获得的最大金额是: 不偷第 i-1 间房子能获得的最大金额 加上 偷第 i 间房子获得的金额,既 f(i,偷) = f(i-1,不偷) + nums[i]

对于第二种情况,我们不偷第 i 间房子,那对于第 i-1 间房子就没有限制,这种情况能获得的最大金额是: 第 i-1 间房子偷或不偷两种情况中的较大值,既 f(i,不偷) = max(f(i-1,不偷),f(i-1,偷))

以此类推,直到 i 为 0.

使用 flag 来表示是否要偷第 i 间房子

function rob(nums: number[]): number {
  const n = nums.length
  const helper = (i: number, flag: boolean): number => {
    if (i === 0) return flag ? nums[i] : 0

    // 要偷这间房子,则只能不偷第 i-1 间房子
    if (flag) return helper(i - 1, false) + nums[i]
    // 不偷这间房子,则从不偷第 i-1 间房子和偷 i-1 间房子中取较大者既为到这间房间为止能偷的最大值
    else return Math.max(helper(i - 1, false), helper(i - 1, true))
  }

  return Math.max(helper(n - 1, false), helper(n - 1, true))
}
  • 时间复杂度: O(2n)O(2^n)
  • 空间复杂度: O(n)O(n)

时间复杂度: 这个时间复杂度不是很容易推导,不过可以用一个数组去统计每层被访问的次数,会发现结果是一个裴波那契数列,总的时间复杂度是趋近于指数级的.

| i   | 0(不偷) | 1(偷) |
| --- | ------- | ----- |
| 1   | 10946   | 6765  |
| 2   | 6765    | 4181  |
| 3   | 4181    | 2584  |
| 4   | 2584    | 1597  |
| 5   | 1597    | 987   |
| 6   | 987     | 610   |
| 7   | 610     | 377   |
| 8   | 377     | 233   |
| 9   | 233     | 144   |
| 10  | 144     | 89    |
| 11  | 89      | 55    |
| 12  | 55      | 34    |
| 13  | 34      | 21    |
| 14  | 21      | 13    |
| 15  | 13      | 8     |
| 16  | 8       | 5     |
| 17  | 5       | 3     |
| 18  | 3       | 2     |
| 19  | 2       | 1     |
| 20  | 1       | 1     |

空间复杂度: 如果计算递归栈的话,则是 O(n)O(n),如果不算递归栈的话,则是 O(1)O(1)

优化时间: 记忆化搜索

上面的解法中,会存在大量重复的计算,时间复杂度是恐怖的指数级,我们可以通过添加缓存来优化.

为了方便,flag 使用 0 或者 1 表示,0 表示不偷,1 表示偷

function rob(nums: number[]): number {
  const n = nums.length
  const cache: number[][] = new Array(n).fill(0).map(() => [0, 0])
  const helper = (i: number, flag: 0 | 1): number => {
    if (i === 0) return flag ? nums[i] : 0
    if (cache[i][flag]) return cache[i][flag]

    cache[i][flag] = flag
      ? helper(i - 1, 0) + nums[i]
      : Math.max(helper(i - 1, 0), helper(i - 1, 1))

    return cache[i][flag]
  }

  return Math.max(helper(n - 1, 0), helper(n - 1, 1))
}
  • 时间复杂度: O(n)O(n)
  • 空间复杂度: O(n)O(n)

时间复杂度: 优化后每间房子只会被访问两次,所以时间复杂度是 O(2n)O(2n),一般会省略前面的常数,直接表示为 O(n)O(n)

空间复杂度: 缓存使用了 O(n)O(n) 的空间

自底向上

function rob(nums: number[]): number {
  const n = nums.length
  const helper = (i = 0, sum: [number, number] = [0, 0]): number => {
    if (i === nums.length) return Math.max(...sum)

    return helper(i + 1, [Math.max(...sum), sum[0] + nums[i]])
  }

  return helper()
}
  • 时间复杂度: O(n)O(n)
  • 空间复杂度: O(n)O(n)

动态规划

可以直接用上面记忆化搜索中的缓存,作为动态规划的状态

  • dp[i]: 第 i 间能偷到的最大金额,其中 dp[i][0] 表示不偷第 i 间能偷的最大金额, dp[i][1] 表示偷第 i 间能偷的最大金额
  • 递推公式:
    • dp[i][0]=max(dp[i-1][0],dp[i-1][1])
    • dp[i][1]=dp[i-1][0]+nums[i]
  • 边界:
    • dp[0]=[0,nums[0]]
function rob(nums: number[]): number {
  const n = nums.length
  const dp: number[][] = new Array(n).fill(0).map(() => [])
  dp[0] = [0, nums[0]]
  for (let i = 1; i < nums.length; i++) {
    dp[i] = [Math.max(...dp[i - 1]), dp[i - 1][0] + nums[i]]
  }

  return Math.max(...dp[n - 1])
}
  • 时间复杂度: O(n)O(n)
  • 空间复杂度: O(n)O(n)

空间优化

可以发现第 i 次的状态只于第 i-1 次的状态有关,所以可以进行状空间优化,使用两个变量代替 dp 数组来保存状态

function rob(nums: number[]): number {
  let [num1, num2] = [0, nums[0]]
  for (let i = 1; i < nums.length; i++) {
    ;[num1, num2] = [Math.max(num1, num2), num1 + nums[i]]
  }

  return Math.max(num1, num2)
}
  • 时间复杂度: O(n)O(n)
  • 空间复杂度: O(n)O(n)

还有另外一种状态的定义: dp[i] 直接定义到第 i 间房子时能偷的最大金额,则dp[i]=max(dp[i-1],dp[i-2]+nums[i])


打家劫舍系列: