LeetCode198 打家劫舍

54 阅读2分钟

leetcode.cn/problems/ho…

image.png

解法一:自顶向下的递归DP + 备忘录

分析盗贼从左到右走过这一排房子,在每间房子前都有两种选择:抢或者不抢。

  • 如果抢了这间房子,那么他肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。
  • 如果不抢这件房子,那么他可以走到下一间房子前,继续做选择。

如何列出正确的状态转移方程?

  1. 确定【状态】:盗贼每次走过的房子索引
  2. 确定【选择】:抢或不抢
  3. 确定dp函数定义:dp(nums, i)表示在nums[i...]这些房子中能抢到的最大值

为了求最大值,每次在两个选择中,都选更大的结果。当他走过了最后一间房子后,就没得抢了,能抢到的钱显然是 0(base case)。

明确了状态转移方程后,就可以发现对于同一位置,可能会在多条路径中被重复经过,是存在【重叠子问题】的,因此可以引入备忘录优化。 image.png

func rob(nums []int) int {
    memo := make([]int, len(nums))
    for idx := range memo{ // 备忘录初始化为和可能答案不冲突的
        memo[idx] = -1
    }
    // 从第一间屋子开始
    return dp(nums, 0, memo)
}

func dp(nums []int, index int, memo []int) int{
    if index >= len(nums){ // 到最后一个位置时,下一层递归index可能+1,也可能+2,所以要>=
        return 0
    }
    if memo[index] != -1{ // 备忘录避免重复计算
        return memo[index]
    }
    memo[index] = max(
        dp(nums, index+1, memo), // 选择不抢,去下家
        nums[index] + dp(nums, index+2, memo), // 选择抢,只能去下下家
    )
    return memo[index]
}

func max(a, b int) int{
    if a > b{
        return a
    }
    return b
}

解法二:自底向上的递推DP

确认dp数组的含义即可

func rob(nums []int) int {
    // dp[i]表示从第 i间房子开始抢劫,最多能抢到的钱
    dp := make([]int, len(nums)+2) // 在最后一个位置n,下一次选择最远可尝试到达n+2
    // base case,走到最后一个房子时,下一次不管选择下家,还是下下家都没有答案
    dp[len(nums)] = 0
    dp[len(nums)+1] = 0
    for i := len(nums)-1; i>=0; i--{
        dp[i] = max(dp[i+1], nums[i] + dp[i+2]) 
    }
    // 从第1个房子开始抢
    return dp[0]
}

func max(a, b int) int{
    if a > b{
        return a
    }
    return b
}

状态转移每次只和 dp[i] 最近的两个状态有关,所以可以进一步优化,将空间复杂度降低到 O(1)。

func rob(nums []int) int {
    // 记录 dp[i+1] 和 dp[i+2]
    dp_i_1, dp_i_2 := 0, 0
    // 初始化 dp[i]
    dp_i := 0
    for i := len(nums) - 1; i >= 0; i-- {
        dp_i = max(dp_i_1, nums[i] + dp_i_2)
        dp_i_2 = dp_i_1
        dp_i_1 = dp_i
    }
    return dp_i
}

func max(a, b int) int{
    if a > b{
        return a
    }
    return b
}