动规五部曲:
- dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
- 确定递推公式:决定dp[i]的因素就是第i房间偷还是不偷, dp[i] = max(dp[i-2]+nums[i], dp[i-1])
- dp数组如何初始化: 从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]。从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
- 确定遍历顺序:dp[i] 是根据dp[i - 2]和dp[i - 1]推导出来的,那么一定是从前到后遍历!
- 打印检查
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
dp = [0]*(len(nums))
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
return dp[-1]
难点在于成环后需要考虑的情况:
- 不包含首尾元素
- 包含首元素,不包含尾元素
- 包含尾元素,不包含首元素
注意:情况2和3实际上是包含情况1的,因此需要做的是基于#198这道题,分别对于两种情况进行求解,最终取最大值
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
res1 = self.robBase(nums, 0, len(nums)-2)
res2 = self.robBase(nums, 1, len(nums)-1)
return max(res1, res2)
def robBase(self, nums, start, end):
if start == end:
return nums[start]
dp = [0]*len(nums)
dp[start] = nums[start]
dp[start+1] = max(nums[start], nums[start+1])
for i in range(start+2, end+1):
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
return dp[end]
代码难点:
- 注意将#198逻辑进行抽象,如何利用start和end来控制数组
思路:
- 树的遍历方式:前中后序遍历(深度优先搜索DFS)和层序遍历(广度优先搜索BFS)
- 与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”)
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
if root is None:
return 0
if root.left is None and root.right is None:
return root.val
#偷父节点: 两个孩子就不能动,要考虑孙子
val1 = root.val
if root.left:
val1 += self.rob(root.left.left) + self.rob(root.left.right)
if root.right:
val1 += self.rob(root.right.left) + self.rob(root.right.right)
#不偷父节点: 则考虑两个孩子
val2 = self.rob(root.left) + self.rob(root.right)
return max(val1, val2)
注意:
这个递归的过程中其实是有重复计算了。在计算root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,此时计算左右孩子的时候其实又把孙子计算了一遍。因此出现大量重复计算,这也是记忆化递归的用武之地。
什么是记忆化递归?
- 使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。(就是算过的存下来直接用)
- 如果有结果,就直接拿来用;在最终返回前将结果保存到map里
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
memory = {}
def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
if root is None:
return 0
if root.left is None and root.right is None:
return root.val
if self.memory.get(root): #有结果就直接拿来用
return self.memory[root]
#偷父节点: 两个孩子就不能动,要考虑孙子
val1 = root.val
if root.left:
val1 += self.rob(root.left.left) + self.rob(root.left.right)
if root.right:
val1 += self.rob(root.right.left) + self.rob(root.right.right)
#不偷父节点: 则考虑两个孩子
val2 = self.rob(root.left) + self.rob(root.right)
self.memory[root] = max(val1, val2) #将结果保存到map里
return max(val1, val2)
- 在上面两种方法,其实对一个节点偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
- 树形dp,因此递归三部曲 + 动规五部曲
递归三部曲1. 确定递归函数的参数和返回值:
- 要求一个节点偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组,参数为当前节点。
- (结合五部曲来看,返回的其实就是dp数组;这样dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。所以本题dp数组就是一个长度为2的数组,因为在递归的过程中,系统栈会保存每一层递归的参数。(个人理解就是不需要像之前的动规题那样维护一个较长的数组,递归允许只维护两个状态且动态更新))
递归三部曲2. 确定终止条件:
- 在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回。
- (结合五部曲来看,相当于dp数组的初始化)
确定遍历顺序:
- 首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。
- 通过递归左节点,得到左节点偷与不偷的金钱。
- 通过递归右节点,得到右节点偷与不偷的金钱。
递归三部曲3. 确定单层递归的逻辑:
也是确定dp[i]的递推公式:
- 如果是偷当前节点,那么左右孩子就不能偷,val1 = cur.val + left[0] + right[0](参考第一步dp数组含义,下标为0记录不偷该节点所得到的的最大金钱);
- 如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]); 难道left[0]还会比left[1]大?最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
打印检查
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
# dp数组(dp table)以及下标的含义:
# 1. 下标为 0 记录 **不偷该节点** 所得到的的最大金钱
# 2. 下标为 1 记录 **偷该节点** 所得到的的最大金钱
dp = self.traversal(root)
return max(dp)
def traversal(self, node):
# 递归终止条件,就是遇到了空节点,那肯定是不偷的
if not node:
return (0, 0)
left = self.traversal(node.left)
right = self.traversal(node.right)
# 不偷当前节点, 偷子节点
val_0 = max(left[0], left[1]) + max(right[0], right[1])
# 偷当前节点, 不偷子节点
val_1 = node.val + left[0] + right[0]
return (val_0, val_1)