代码随想录训练营 Day39打卡 动态规划 part07
一、力扣198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 :
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i]
如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点)
然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
输入[2,7,9,3,1]为例。
代码实现
class Solution:
def rob(self, nums: List[int]) -> int:
# 如果没有房屋,直接返回0
if len(nums) == 0:
return 0
# 如果只有一个房屋,那么最优解就是抢劫这个房屋,返回其金额
if len(nums) == 1:
return nums[0]
# 创建一个动态规划数组 dp,dp[i] 表示考虑到第 i 个房屋时,能够偷窃到的最大金额
dp = [0] * len(nums)
# 初始化 dp 数组
dp[0] = nums[0] # 只有一个房屋时,只能选择偷这个房屋
dp[1] = max(nums[0], nums[1]) # 两个房屋时,选择金额较大的房屋偷
# 遍历剩余的房屋,从第 2 个房屋开始
for i in range(2, len(nums)):
# 对于每个房屋,计算两种选择的最大值:
# 1. 偷当前房屋(第 i 个),那么之前的选择只能到第 i-2 个房屋,因此金额为 dp[i-2] + nums[i]
# 2. 不偷当前房屋,选择之前的最大金额 dp[i-1]
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
# 返回最后一个房屋对应的 dp 值,即为能够偷窃到的最大金额
return dp[-1]
二、力扣 213. 打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 :
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
对于一个数组,成环的话主要有如下三种情况:
情况一:考虑不包含首尾元素
情况二:考虑包含首元素,不包含尾元素
情况三:考虑包含尾元素,不包含首元素
注意我这里用的是 “考虑” ,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。
而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。
代码实现
class Solution:
def rob(self, nums: List[int]) -> int:
# 如果没有房屋,返回 0
if len(nums) == 0:
return 0
# 如果只有一个房屋,直接返回该房屋的金额
if len(nums) == 1:
return nums[0]
# 情况一:不偷第一个房屋,考虑从第 2 个房屋到最后一个房屋(索引从 1 到 len(nums)-1)
result1 = self.robRange(nums, 1, len(nums) - 1)
# 情况二:不偷最后一个房屋,考虑从第一个房屋到倒数第二个房屋(索引从 0 到 len(nums)-2)
result2 = self.robRange(nums, 0, len(nums) - 2)
# 返回两种情况的最大值
return max(result1, result2)
# robRange 函数实现的是经典的打家劫舍问题,即不考虑房屋成环的情况
def robRange(self, nums: List[int], start: int, end: int) -> int:
# 如果范围内只有一个房屋,直接返回该房屋的金额
if end == start:
return nums[start]
# prev_max 表示偷到前一个房屋的最大金额
prev_max = nums[start]
# curr_max 表示偷到当前房屋为止的最大金额
curr_max = max(nums[start], nums[start + 1])
# 从第 start+2 个房屋开始遍历到 end 房屋
for i in range(start + 2, end + 1):
# 临时存储 curr_max,因为 curr_max 将更新为新的值
temp = curr_max
# 当前最大金额等于偷当前房屋和不偷当前房屋两种情况的最大值
curr_max = max(prev_max + nums[i], curr_max)
# 更新 prev_max 为之前的 curr_max
prev_max = temp
# 返回最终偷窃的最大金额
return curr_max
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
三、力扣 337. 打家劫舍III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 :
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
代码实现
我们可以通过后序遍历的方式来解决这个问题。对于每个节点,我们都有两个选择:
- 不偷当前节点:那么我们可以选择偷它的左右子节点。
- 偷当前节点:那么我们不能偷它的左右子节点。
我们用一个长度为 2 的数组 dp 来表示当前节点的状态:
dp[0] 表示 不偷当前节点 时的最大盗窃金额。
dp[1] 表示 偷当前节点 时的最大盗窃金额。
我们需要计算两个值:
val_0:不偷当前节点时,可以从它的子节点中得到的最大金额。
val_1:偷当前节点时,只能加上它的左右子节点不偷时的最大金额。
以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导)
最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱。
# 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 = self.traversal(root)
# 返回两种情况的最大值,即为可以偷窃的最高金额
return max(dp)
def traversal(self, node: Optional[TreeNode]) -> tuple:
# 如果节点为空,返回 (0, 0) 表示不偷和偷的情况下金额都为 0
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)