代码随想录27期|Python|Day45|​动态规划 |​ 198.打家劫舍| 213.打家劫舍II |337.打家劫舍III

本章全部是打家劫舍的题型。

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完结!!!

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值