LeetCode 198.打家劫舍:
思路:
当前房屋抢劫得到的金额依赖于前一个房屋是否抢劫,是动态规划问题。
动规五部曲:
- dp数组及含义:
dp[i] : 在序号为[0, i]的房屋中抢劫得到的最大金额 - 递推公式:
根据是否抢劫序号为 i 的房屋来分类:
① 如果抢劫第 i 房屋,由于相邻的房屋不能同一晚上被抢劫,因此 i - 1号房屋不能抢劫。因此最大金额为[0, i - 2]号房屋抢劫到的最大金额+第 i 房屋的钱
dp[i] = dp[i - 2] + nums[i]
② 如果不抢劫第 i 房屋,那么最大金额为[0, i - 1]号房屋抢劫到的最大金额,其中不知道第 i - 1房屋是否被抢劫
dp[i] = dp[i - 1]
① 和 ② 取最大值:
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) - 初始化:
由递推公式可以看出,dp[i]取决于dp[i - 1]和dp[i - 2],因此应当初始化dp[0]和dp[1]。因为房屋的钱最少是0,因此
dp[0] = nums[0],dp[1] = max(nums[0], nums[1]) - 遍历方式:
由递推公式可得,遍历为从前往后 - 举例:
代码:
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) <= 1:
if len(nums) == 0:
return 0
return nums[0]
dp = [0] * (len(nums))
# 初始化
dp[0], dp[1] = nums[0], 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]
感悟:
首先分析出是动态规划类型的题目,然后是动规五部曲
LeetCode 213.打家劫舍Ⅱ:
思路:
本题与上题相似,不同之处在于nums数组变成了环,数组变成环,那么特殊的就是首尾元素,处理方法如下:
由于情况2,3包括了情况1,因此只需要分别求出情况2和情况3的最大金额,再选择其中的最大值即可
# 下面代码中将之前打家劫舍的代码独立了出来,再求情况2,3的最大金额
class Solution:
def rob(self, nums: List[int]) -> int:
# 误差情况,给定的nums有只有一个元素的情况
if len(nums) <= 1:
if len(nums) == 0:
return 0
return nums[0]
# nums有多个元素的情况
result1 = self.robRange(nums, 0, len(nums) - 2) # 考虑首不考虑尾
result2 = self.robRange(nums, 1, len(nums) - 1) # 不考虑首考虑尾
return max(result1, result2)
# 单独的打家劫舍的函数,求[start, end]序号的房屋所能打劫到的最大金额
def robRange(self, nums, start, end):
# 如果nums只有两个元素,按照前面的话出现start == end
if start == end:
return nums[start]
# 初始化
dp = [0] * (end - start + 1) # 注意这里nums[start]对应dp[0]
dp[0] = nums[start]
# print("len(nums): " + str(len(nums)) + "start: " + str(start)+"end: " + str(end))
dp[1] = max(nums[start], nums[start + 1])
# 遍历,所以遍历的时候dp应当为dp[i - start]
for i in range(start + 2, end + 1):
dp[i - start] = max(dp[i-start - 2] + nums[i], dp[i -start- 1])
return dp[-1]
感悟:
数组成环后的处理方式是分情况讨论处理,以及代码处理中的细节,独立出来的打家劫舍函数中dp[0]和nums[start]的对应,从而在后续遍历时,dp应当为dp[i - start]
LeetCode 337.打家劫舍Ⅲ:
思路1:动态规划
首先分析题目,是在一个树状结构上面进行打家劫舍,且两个直接相连的房子不能同时偷窃。
那么还是分析偷不偷当前节点的情况,我们考虑的当前节点是如下这种情况:
- 如果要偷当前节点,那么左右节点就不能偷,偷以当前节点为根节点的树得到的最大金额就是cur.val + 不偷左右节点后左右子树的最大金额
- 如果不偷当前节点,那么考虑偷左右节点,偷以当前节点为根节点的树得到的最大金额就是左子树最大金额 + 右子树最大金额(其中左右子树最大金额不一定偷了左右节点)
再考虑树的遍历顺序(深度or广度),因此本题中应当采用后序遍历。
递归三部曲 + 动态规划 - 参数和返回值:
传入参数为node,因为考虑当前节点的最大金额需要用到偷 or 不偷孩子节点的情况,因此返回值应当为只有两个元素的数组dp,dp[0]为不偷当前节点的最大金额,dp[1]为偷当前节点的最大金额。同时这个dp也是动态规划的dp(因为递归会保存返回值) - 边界条件:
node为None,返回**(0, 0)** - 正常递归:
后序遍历,先遍历左右子树,再分别得到偷 or 不偷当前节点的最大金额
代码:
# 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:
if root == None:
return 0
dp = self.traversal(root)
return max(dp[0], dp[1])
def traversal(self, node):
# 边界条件
if node is None:
return (0, 0)
# 正常递归
left = self.traversal(node.left) # 得到左子树不偷和偷左节点的最大金额
right = self.traversal(node.right) # 同上
# 得到当前节点不偷和偷当前节点的最大金额
val0 = max(left[0], left[1]) + max(right[0], right[1]) # 不偷当前节点
val1 = node.val + left[0] + right[0]
return (val0, val1)
思路2:递归+记录节点最大金额
由上面分析可知,遍历这棵树应当采用后序遍历。同时我们采用递归的方式。
还是根据是否偷当前节点来进行分析:
- 如果偷当前节点node
那么左右孩子节点就不能偷,最大金额是node.val + (左子树不偷根节点最大金额) + (右子树不偷根节点最大金额)
左子树不偷根节点最大金额(如果左节点存在) = function(node.left.left) + function(node.left.right) - 如果不偷当前节点node
那么左右孩子节点就能偷,最大金额是function(node.left) + function(node.right)(左右子树最大金额之和,最大金额不一定偷了左右孩子节点)
而分析上面发现,递归过程有许多重复的部分,因此我们采用map映射保存递归得到的以当前节点为根节点的树的最大金额。
递归三部曲: - 参数和返回值:
参数node,返回以node为根节点的树的最大金额 - 边界条件:
node为None,或者node为叶子节点,或者当前节点已经求过最大金额了,即map中存在当前节点对应的最大金额 - 正常递归:
就是前面是否偷当前节点的分析
代码:
# 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:
memory = {}
def rob(self, root: Optional[TreeNode]) -> 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) is not None: # 之前求过了
return self.memory[root]
# 正常递归
# 偷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)
# 不偷root节点
val0 = self.rob(root.left) + self.rob(root.right)
# 记录当前节点的最大金额
self.memory[root] = max(val0, val1)
return self.memory[root]
感悟:
根据题目分析应当采用树的什么遍历顺序,以及将当前节点当作根节点分析是否偷当前树,因为采用的递归,dp数组大小为2,保存偷当前节点和不偷当前节点的最大金额。(最后dp数组有点难得想到)
学习收获:
动态递归典型:打家劫舍基础 + 环状 + 树状