解法一:自顶向下的递归DP + 备忘录
分析盗贼从左到右走过这一排房子,在每间房子前都有两种选择:抢或者不抢。
- 如果抢了这间房子,那么他肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。
- 如果不抢这件房子,那么他可以走到下一间房子前,继续做选择。
如何列出正确的状态转移方程?
- 确定【状态】:盗贼每次走过的房子索引
- 确定【选择】:抢或不抢
- 确定dp函数定义:dp(nums, i)表示在nums[i...]这些房子中能抢到的最大值
为了求最大值,每次在两个选择中,都选更大的结果。当他走过了最后一间房子后,就没得抢了,能抢到的钱显然是 0(base case)。
明确了状态转移方程后,就可以发现对于同一位置,可能会在多条路径中被重复经过,是存在【重叠子问题】的,因此可以引入备忘录优化。
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
}