本章全部是打家劫舍的题型。
198. 打家劫舍
这个题和之前的完全遍历不同,这个中间空格1个才能取。
1、dp数组定义
采用一维数组,dp[i]表示当前到第i个物品的时候,所能取得的最大值(i物品不一定被取);
2、确定递推关系
dp[i] = max(dp[i-2]+nums[i], dp[i-1]);包含两个值,
第一个部分是如果取第i个,那么就要取还是第i-2物品的时候的状态,加上当前i物品的值;
第二个部分是如果不取i的话,那么dp仍然保持i-1的状态,两个值比较取较大。
3、确定初始化
全局初始化:max操作而且全部都是非负数,所以全部归0即可,因为递推公式其实是根据前面的值来确定的;
开头初始化:第0个物品肯定dp就是它的值,第1个物品的值就取决于它和第0个物品的值谁大取谁。
4、确定遍历顺序
根据递推公式,由于只涉及物品,而没有背包的重量,所以从小到大遍历物品就可以。
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
dp = [0] * (len(nums)+1)
dp[0] = nums[0] # 看到索引0的时候只有这一个值
if len(nums) > 1:
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[-2]
213. 打家劫舍 II
本题多加了一个不能闭环的要求,实际上可以拆分成三种情况:
(1)索引0和倒数第二个(-2)都取了,此时返回遍历到倒数第二个地方的最大值;
(2)索引1和倒数第一个(-1)都取了,此时返回遍历到倒数第一个地方的最大值;
(3)0和-1都没有被取到,只有中间部分被取到了,实际上包含在了以上两个以内。
最后比较dp数组最后返回值的大小即可。
以上每种情况的算法结构是一致的,所以可以被封装为一个单独的函数。实际上就是上一题的算法。然后分别返回(1)(2)的结果,比较较大的返回。
在传递参数的时候一定注意取与不取的细节。还有最后返回的dp值是传入的end索引,而不是初始化的dp最后一个值。
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.Rob(0, len(nums)-2, nums) # 从0开始,到倒数第二个元素的下标
res2 = self.Rob(1, len(nums)-1, nums) # 从1开始,到倒数第一个元素的下标
return max(res1, res2) # 返回交大值
def Rob(self, start, end, nums):
if start == end: # 长度为1的数组
return nums[start]
dp = [0] * len(nums)
# 第1个和第2个元素的初始化
dp[start] = nums[start]
dp[start+1] = max(nums[start], nums[start+1])
for i in range(start+2, end+1): # 保证i-2操作和end索引的位置元素可以被取到
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
return dp[end] # 返回的是传入的末尾索引值
337. 打家劫舍 III
本题的难点在于:
1、如果递归的话,会出现超时的情况(不同于搜索);
2、如果采用动态规划的话,如何定义dp数组和递推公式。
1、暴力递归+记忆递归
按照二叉树的后序遍历(左右中)的思路由下而上遍历(父节点所能够取到的最大值取决于子节点)。弊端:实时计算当前值,出现重复计算开销。
确定终止条件:当前节点是null或者当前节点是叶子节点;
比较两个值:1、不取当前的节点,则取子节点的最大值作为当前节点的值;2、取当前的节点,则跳过子节点,加上孙子节点的值。
遍历顺序:左右中;
返回值:一个int,表示当前所能够取的最大值(取与不取)。
但是,直接写一个递归会超时,所以采用一张mem_map(字典),来储存每一个已经被遍历的node的最大值。
# 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):
mem_map = {} # 全局初始化记忆表
def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
if root is None:
return 0 # 不存在的节点返回0
if root.right is None and root.left is None:
return root.val # 叶子节点返回当前值
if self.mem_map.get(root):
return self.mem_map[root] # 已经计算过的节点直接返回
dp = [0, root.val] # 对每一个节点初始化一个dp数组,索引0表示不偷,1表示偷
# 1、如果要偷窃当前节点
if root.left: # 左节点存在,跳到孙子及节点
dp[1] += self.rob(root.left.right) + self.rob(root.left.left)
if root.right: # 右节点也是
dp[1] += self.rob(root.right.right) + self.rob(root.right.left)
# 2、不偷当前节点
dp[0] = self.rob(root.left) + self.rob(root.right)
self.mem_map[root] = max(dp[0], dp[1]) # 储存当前节点的价值最大值
return self.mem_map[root] # 返回当前节点的最大值
2、动态规划
动态规划的思路和上面大概一致,主要是dp的含义。
1、确定dp含义:对于每一个节点,都给他一个二元数组dp,dp[0]表示不偷当前节点的值,dp[1]表示偷了当前节点的值。
2、确定递推公式:分两种情况:
(1)偷当前节点:当前节点值+left节点不偷的值+right节点不偷的值;
(2)不偷当前节点:左节点dp的最大值+右节点dp的最大值。
3、确定初始化:所有节点的dp都初始化为0,0即可。这里注意,在递归的函数体里面,需要初始化一个left和right的dp来暂时储存当前节点左右子节点的dp返回值。
4、确定遍历顺序:对于树动态规划,没有for循环遍历物品和背包,根据树的结构确定遍历顺序(前还是后)。本题是后序。
# 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):
mem_map = {} # 全局初始化记忆表
def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
dp = self.traversal(root) # dp数组是一个二元数组,储存取与不取的最大值
return max(dp)
def traversal(self, node):
if node is None:
return [0, 0] # 当前是空节点,返回空的dp
# 对父节点初始化两个字节点的dp
left = [0, 0]
right= [0, 0]
# 递归两个子节点的dp
left = self.traversal(node.left)
right = self.traversal(node.right)
# 1、不偷当前的节点,分别取两个子节点的dp最大值(包含偷和不偷子节点)相加
val_1 = max(left) + max(right)
# 2、偷当前节点,取当前节点值,加上子节点dp数组不偷的值
val_2 = node.val + left[0] + right[0]
return [val_1, val_2] # 返回当前节点的dp
Day45完结!!!