动态规划篇 —— 打家劫舍
今天总结打家劫舍问题
题目链接
状态定义:dp[i]表示考虑到下标i为止,所能偷到的最高金额
状态转移:关键是考虑 偷不偷nums[i]
如果偷当前的nums[i]的话,就是dp[i-2]+nums[i], 如果不偷nums[i]的话就要考虑 dp[i-1]
所以 dp[i] = max(dp[i-2] + nums[i], dp[i-1])
base case: 根据递推公式就能发现 dp[0], dp[1]是需要先初始化的
dp[0] = nums[0], dp[1] = max(dp[0], dp[1])
遍历方向以及解的位置:
遍历方向就应该是正序,解的位置就是考虑到下标len(nums)-1 为止之前的所有情况的最大值
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: 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[len(nums)-1]
题目链接
这道题和上一道的区别就在于这道题的数组是成环的
成环的话我们需要适当对数组进行裁剪
第一种,考虑包含首元素,不包含尾元素
我们考虑的数组本身就不包含尾元素,不管我们选不选第一个,就都没问题了
第二种,考虑包含尾元素,不包含首元素
对数组进行裁剪之后,再对裁剪后的数组各自进行打家劫舍,运算的结果比较出一个最大的就行了
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: return nums[0]
if len(nums) == 2: return max(nums[0], nums[1])
def dprange(nums):
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]
res1, res2 = dprange(nums[1:]), dprange(nums[:-1])
return max(res1, res2)
题目链接
这个题还考察对二叉树的遍历,我发现我还是有点怕的,可能写的少吧
# 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: TreeNode) -> int:
self.mem= {}
def dfs(root):
if not root:
return 0
if root in self.mem:
return self.mem[root]
ans1 = root.val #选根和根孙子
if root.left:
ans1 += (dfs(root.left.left) + dfs(root.left.right))
if root.right:
ans1 += (dfs(root.right.left) + dfs(root.right.right))
ans2 = dfs(root.left) + dfs(root.right) #不选根 选根子树
res = max(ans1, ans2)
self.mem[root] = res
return res
return dfs(root)
时间复杂度:O(n),每个节点只遍历了一次
空间复杂度:O(log n),算上递推系统栈的空间
动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
1.确定递归函数参数以及返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。参数就是node或者命名cur
dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
所以本题dp数组就是一个长度为2的数组!
那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?
别忘了在递归的过程中,系统栈会保存每一层递归的参数。
2.确定终止条件也就是出口
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
这也相当于dp数组的初始化
if not node:
return (0, 0)
3.确定树的递归遍历顺序
首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
left = dfs(node.left)
right = dfs(node.right)
4.确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就在回顾一下dp数组的含义)
为什么要cur.val + left[0] + right[0]呢? 我觉得因为是左右根的递归顺序,左边这个子树的状态,右边子树的状态算完,给根节点赋值
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
# 不偷当前节点, 偷子节点
val_0 = max(left[0], left[1]) + max(right[0], right[1])
# 偷当前节点, 不偷子节点
val_1 = node.val + left[0] + right[0]
return (val0, 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:
if not root: return 0
if not root.left and not root.right: return root.val
def dfs(node):
if not node: return (0,0)
left = dfs(node.left)
right = dfs(node.right)
#不偷当前结点,偷子树
val0 = max(left[0], left[1]) + max(right[0], right[1])
#偷当前结点, 不偷子结点
val1 = node.val + left[0] + right[0]
return (val0, val1)
dp = dfs(root)
return max(dp)
时间复杂度:O(n),每个节点只遍历了一次
空间复杂度:O(log n),算上递推系统栈的空间