题目链接 | 理论基础
经典的 dp 问题,重点在于记录访问过的元素的状态。
dp 数组的下标含义:dp[i][j]
dp[i][0]
表示不抢房屋 i 的情况下,从房屋 [0, i] 中抢到的最大金额dp[i][1]
表示抢房屋 i 的情况下,从房屋 [0, i] 中抢到的最大金额dp 递推公式:
dp[i][0] = max(dp[i-1][0], dp[i-1][1])
,从房屋 [0, i - 1] 中选择更优解dp[i][1] = dp[i-1][0] + nums[i]
,则房屋 i - 1 肯定不能抢dp 数组的初始化:只需要初始化房屋 i=0
,其他的值会在递推中更新
dp[0][0]=0
,抢了就是 dp[0][1]=nums[0]
dp 的遍历顺序:当前房屋 i 完全依赖于 房屋 i - 1 的情况,所以从小到大按顺序遍历即可
举例推导:
1 | 2 | 3 | 1 | |
---|---|---|---|---|
0 | 0 | 1 | 2 | 4 |
1 | 1 | 2 | 4 | 3 |
class Solution:
def rob(self, nums: List[int]) -> int:
# dp[i][0] represents the max money that can be obtained from house [0, i] if house i is not robbed
# dp[i][1] represents the max money that can be obatined from house [0, i] if house i is robbed
dp = [[0, 0] for _ in range(len(nums))]
dp[0][0] = 0
dp[0][1] = nums[0]
for i in range(1, len(nums)):
dp[i][0] = max(dp[i-1][0], dp[i-1][1])
dp[i][1] = dp[i-1][0] + nums[i]
return max(dp[-1][0], dp[-1][1])
实际上,无需在意之前节点进行的状态,也可以直接推导。
dp[i]
代表从房屋 [0, i] 中能抢到的最大金额。i
,显然有两种选择:抢或者不抢
dp[i] = dp[i-2] + nums[i]
dp[i] = dp[i-1]
dp[0], dp[1]
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
nums | 2 | 7 | 9 | 3 | 1 |
---|---|---|---|---|---|
dp | 2 | 7 | 11 | 11 | 12 |
class Solution:
def rob(self, nums: List[int]) -> int:
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-1], dp[i-2] + nums[i])
return dp[-1]
题目链接 | 理论基础
和上一题的区别在于首尾元素也是相邻的,所以按照上一题的逻辑推到最后一栋房屋时,实际上不能自由根据收益选择是否要抢,还依赖于第一栋房屋的状态。
实际上,分类就可以解决首尾相连的特殊情况:
nums[1:]
进行上一题。nums[:-1]
进行上一题。分别计算两种情况,然后取最大值即可。
class Solution:
def rob(self, nums: List[int]) -> int:
dp = [[[0, 0] for _ in range(len(nums))] for i in range(2)]
dp[0][0] = [0, float('-inf')]
dp[1][0] = [float('-inf'), nums[0]]
for i in range(1, len(nums)):
dp[0][i][0] = max(dp[0][i-1][0], dp[0][i-1][1])
dp[0][i][1] = dp[0][i-1][0] + nums[i]
for i in range(1, len(nums) - 1):
dp[1][i][0] = max(dp[1][i-1][0], dp[1][i-1][1])
dp[1][i][1] = dp[1][i-1][0] + nums[i]
if len(nums) >= 2:
dp[1][len(nums)-1] = dp[1][len(nums)-2]
return max(dp[0][-1][0], dp[0][-1][1], dp[1][-1][0], dp[1][-1][1])
同样,其实也不需要保存每一个访问过的房屋的状态,直接记录最佳解即可。
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
dp_rob1 = [0] * (len(nums))
dp_no_rob1 = [0] * (len(nums))
# leave house 0 unrobbed, considering nums[1:]
dp_rob1[0] = 0
dp_rob1[1] = nums[1]
for i in range(2, len(nums)):
dp_rob1[i] = max(dp_rob1[i-1], dp_rob1[i-2] + nums[i])
# rob house 0, leave the last house safe, considering nums[:-1]
dp_no_rob1[0] = nums[0]
dp_no_rob1[1] = max(nums[0], nums[1])
for i in range(2, len(nums) - 1):
dp_no_rob1[i] = max(dp_no_rob1[i-1], dp_no_rob1[i-2] + nums[i])
dp_no_rob1[-1] = dp_no_rob1[-2]
return max(dp_rob1[-1], dp_no_rob1[-1])
题目链接 | 理论基础
朴素的后序遍历,依靠递归解决这道典型的二叉树。但是递归超高的时间复杂度,导致超时,因为递归中有大量的重复计算。
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
if root == None:
return 0
skip_root = self.rob(root.left) + self.rob(root.right)
rob_root = root.val
if root.left != None:
rob_root += self.rob(root.left.left) + self.rob(root.left.right)
if root.right != None:
rob_root += self.rob(root.right.left) + self.rob(root.right.right)
return max(skip_root, rob_root)
很明显,暴力递归对子问题的重复计算导致了超时,所以我们希望使用 dp 来记录已经得到的子问题的最优解,不断递归来得到全局的最优解。由于二叉树需要递归,所以返回值就是用来记录 dp 的(状态,最优解)组合。
递归三部曲:
dp[0]
记录了如果不抢当前节点的最大收益,dp[1]
记录了如果抢当前节点的最大收益[0, 0]
,因为抢和不抢都是没有收益left_dp[0] + right_dp[0] + root.val
max(left_dp[0], left_dp[1]) + max(right_dp[0], right_dp[1])
class Solution:
def dpRob(self, root: Optional[TreeNode]) -> [int, int]:
# return[0] represents the max money if root is not taken
# return[1] represents the max money if root is taken
if root == None:
return [0, 0]
left_dp = self.dpRob(root.left)
right_dp = self.dpRob(root.right)
skip_root = max(left_dp[0], left_dp[1]) + max(right_dp[0], right_dp[1])
rob_root = left_dp[0] + right_dp[0] + root.val
return [skip_root, rob_root]
def rob(self, root: Optional[TreeNode]) -> int:
dp_root = self.dpRob(root)
return max(dp_root[0], dp_root[1])
树形 dp!一个新的题目,本质还是二叉树 + dp 的组合,就像监控二叉树一样,考察不同知识点的结合掌握。
对于 dp 来说,二叉树的递归性质反而决定了题目更简单,因为递归只允许返回当前节点的状态。
最后一道打家劫舍,终于需要记录访问过的节点状态啦。