Leetcode打家劫舍系列题
打家劫舍是动态规划的经典题,虽然可以用其他方法解,但是在本文主要介绍动态规划的解法。
198.打家劫舍
题目描述
分析
动态规划在于找转移方程
- 找出口
- 如果只有一家,直接偷就完事了
- 如果有两家,看两家哪家偷得多
- 转移方程
- 如果n=3, 那么两种情况,偷第二家和偷第一,第三家
nums[3] += nums[1]- n>3时,从第四家开始,前一家有可能来源于前面的第二家,或者第三家,取决于哪家偷得多。
- 遍历结束,判断倒数第一家和第二家哪家偷的多,返回
代码
func rob(nums []int) int {
if len(nums) == 1 {
return nums[0]
}
if len(nums) == 2 {
return max(nums[0], nums[1])
}
nums[2] += nums[0]
// 从i>2家开始,偷的上一家可能是i-2 或者 i-3 取决于之前哪家偷的多
for i := 3; i < len(nums); i++ {
nums[i] += max(nums[i-2], nums[i-3])
}
// 最后有可能偷最后一家多,也有可能偷倒数第二家多
return max(nums[len(nums)-1], nums[len(nums)-2])
}
func max (a, b int) int {
if a < b {
return b
}
return a
}
213.打家劫舍Ⅱ
题目描述
分析
相较于第一种方式,第二种最后一家和第一家认为是相邻的,也就是不能同时偷这两家。
方法:
分组动态规划,对第 1 家到第 n-1家动态规划,对第二家到第n家动态规划,最后比较两组动态规划的最优解
代码
func rob(nums []int) int {
dp1 := make([]int, len(nums)-1)
dp2 := make([]int, len(nums)-1)
if len(nums) == 1 {
return nums[0]
}
dp1[0] = nums[0]
if len(nums) == 2 {
return max(nums[0], nums[1])
}
dp1[1] = nums[1]
dp2[0] = nums[1]
if len(nums) == 3 {
return max(max(nums[0], nums[1]), nums[2])
}
dp1[2] = nums[2] + dp1[0]
dp2[1] = nums[2]
if len(nums) == 4 {
return max(nums[0] + nums[2], nums[1]+nums[3])
}
dp2[2] = nums[3] + dp2[0]
for i := 3; i < len(nums)-1; i++ {
dp1[i] = max(dp1[i-2], dp1[i-3]) + nums[i]
dp2[i] = max(dp2[i-2], dp2[i-3]) + nums[i+1]
}
// retDP1 方案一的偷盗结果
// retDP2 方案二的偷盗结果
retDP1 := max(dp1[len(dp1)-1], dp1[len(dp1)-2])
retDP2 := max(dp2[len(dp2)-1], dp2[len(dp2)-2])
return max(retDP1, retDP2)
}
func max(a, b int) int {
if a < b {
return b
}
return a
}
337.打家劫舍Ⅲ
题目描述
tips
树形结构,涉及到数据的遍历,所以这题的解法是基于DFS的。
不过我个人觉得,动态规划并非一定要有一个dp存储状态转移量,只要存在状态转移的,都可以认为是动态规划的解法。
所以我觉得本题解法可以称为基于DFS的动态规划
分析
- 题意理解
这一题是打家劫舍系列第三题,这一题需要偷的房子是树形的,报警的条件同样是偷了相邻的则报警。只不过这里的房子在树上,求解小偷在不触发报警的情况下怎么偷得最多的钱。- 基于DFS搜索(左右根),先搜索孩子节点,再判断当前节点。
- 确立状态
- 当前节点有两种状态,被偷(rb),或者不被偷(nrb)
- 如果被偷
- 因为与孩子节点相邻,所以孩子节点不能被偷
- currb = cur.value + L.nrb + R.nrb
- 如果不被偷
- 那么当前节点的状态来源于孩子节点被偷或者不被偷
- curnrb = max(L.rb, L.nrb) + max(R.rb, R.nrb)
代码
func rob(root *TreeNode) int {
// 自定义比较函数 a和b谁大返回谁
max := func(a, b int) int {
if a < b {
return b
}
return a
}
// 定义搜索算法
var dfs func(root *TreeNode) (beRobbed, beNotRobbed int)
dfs = func(root *TreeNode) (beRobbed, beNotRobbed int) {
if root == nil {
return 0, 0
}
// 搜索左子树,lbr表示左孩子节点被打劫, lnbr表示左孩子节点未被打劫
lbr, lnbr := dfs(root.Left)
// 搜索右子树,rbr表示右孩子节点被打劫,rnbr表示右孩子节点未被打劫
rbr, rnbr := dfs(root.Right)
// curbr表示当前节点被打劫
// 当前节点被打劫的话,那么孩子节点就未被打劫
curbr := root.Val + lnbr + rnbr
// curnbr表示当前节点未被打劫
// 当前节点未被打劫 那么孩子节点可能被打劫,也可能不被打劫,取决于哪种情况打劫收入高
curnbr := max(lbr, lnbr) + max(rbr, rnbr)
return curbr, curnbr
}
return max(dfs(root))
}