代码随想录算法训练营第三十九天 | 打家劫舍

LeetCode 198.打家劫舍:

文章链接
题目链接:198.打家劫舍

思路:

当前房屋抢劫得到的金额依赖于前一个房屋是否抢劫,是动态规划问题。
动规五部曲:

  • dp数组及含义:
    dp[i] : 在序号为[0, i]的房屋中抢劫得到的最大金额
  • 递推公式:
    根据是否抢劫序号为 i 的房屋来分类:
    ① 如果抢劫第 i 房屋,由于相邻的房屋不能同一晚上被抢劫,因此 i - 1号房屋不能抢劫。因此最大金额为[0, i - 2]号房屋抢劫到的最大金额+第 i 房屋的钱
    dp[i] = dp[i - 2] + nums[i]
    ② 如果不抢劫第 i 房屋,那么最大金额为[0, i - 1]号房屋抢劫到的最大金额,其中不知道第 i - 1房屋是否被抢劫
    dp[i] = dp[i - 1]
    ① 和 ② 取最大值:
    dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
  • 初始化:
    由递推公式可以看出,dp[i]取决于dp[i - 1]和dp[i - 2],因此应当初始化dp[0]和dp[1]。因为房屋的钱最少是0,因此
    dp[0] = nums[0],dp[1] = max(nums[0], nums[1])
  • 遍历方式:
    由递推公式可得,遍历为从前往后
  • 举例:
    在这里插入图片描述
    代码:
class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) <= 1:
            if len(nums) == 0:
                return 0
            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[-1]

感悟:

首先分析出是动态规划类型的题目,然后是动规五部曲


LeetCode 213.打家劫舍Ⅱ:

文章链接
题目链接:213.打家劫舍Ⅱ

思路:

本题与上题相似,不同之处在于nums数组变成了环,数组变成环,那么特殊的就是首尾元素,处理方法如下:在这里插入图片描述
由于情况2,3包括了情况1,因此只需要分别求出情况2和情况3的最大金额,再选择其中的最大值即可

# 下面代码中将之前打家劫舍的代码独立了出来,再求情况2,3的最大金额
class Solution:
    def rob(self, nums: List[int]) -> int:
        # 误差情况,给定的nums有只有一个元素的情况
        if len(nums) <= 1:
            if len(nums) == 0:
                return 0
            return nums[0]
        # nums有多个元素的情况
        result1 = self.robRange(nums, 0, len(nums) - 2) # 考虑首不考虑尾
        result2 = self.robRange(nums, 1, len(nums) - 1) # 不考虑首考虑尾
        return max(result1, result2)
        

    # 单独的打家劫舍的函数,求[start, end]序号的房屋所能打劫到的最大金额
    def robRange(self, nums, start, end):
        # 如果nums只有两个元素,按照前面的话出现start == end
        if start == end:
            return nums[start]
        # 初始化
        dp = [0] * (end - start + 1)    # 注意这里nums[start]对应dp[0]
        dp[0] = nums[start]
        # print("len(nums): " + str(len(nums)) + "start: " + str(start)+"end: " + str(end))
        dp[1] = max(nums[start], nums[start + 1])
        # 遍历,所以遍历的时候dp应当为dp[i - start]
        for i in range(start + 2, end + 1):
            dp[i - start] = max(dp[i-start - 2] + nums[i], dp[i -start- 1])
        return dp[-1]
        

感悟:

数组成环后的处理方式是分情况讨论处理,以及代码处理中的细节,独立出来的打家劫舍函数中dp[0]和nums[start]的对应,从而在后续遍历时,dp应当为dp[i - start]


LeetCode 337.打家劫舍Ⅲ:

文章链接
题目链接:337.打家劫舍Ⅲ

思路1:动态规划

首先分析题目,是在一个树状结构上面进行打家劫舍,且两个直接相连的房子不能同时偷窃。
那么还是分析偷不偷当前节点的情况,我们考虑的当前节点是如下这种情况:在这里插入图片描述

  • 如果要偷当前节点,那么左右节点就不能偷,偷以当前节点为根节点的树得到的最大金额就是cur.val + 不偷左右节点后左右子树的最大金额
  • 如果不偷当前节点,那么考虑偷左右节点,偷以当前节点为根节点的树得到的最大金额就是左子树最大金额 + 右子树最大金额(其中左右子树最大金额不一定偷了左右节点
    再考虑树的遍历顺序(深度or广度),因此本题中应当采用后序遍历。
    递归三部曲 + 动态规划
  • 参数和返回值:
    传入参数为node,因为考虑当前节点的最大金额需要用到偷 or 不偷孩子节点的情况,因此返回值应当为只有两个元素的数组dp,dp[0]为不偷当前节点的最大金额,dp[1]为偷当前节点的最大金额。同时这个dp也是动态规划的dp(因为递归会保存返回值)
  • 边界条件:
    node为None,返回**(0, 0)**
  • 正常递归:
    后序遍历,先遍历左右子树,再分别得到偷 or 不偷当前节点的最大金额

代码:

# 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 root == None:
            return 0
        dp = self.traversal(root)
        return max(dp[0], dp[1])
        
    def traversal(self, node):
        # 边界条件
        if node is None:
            return (0, 0)
        # 正常递归
        left = self.traversal(node.left)    # 得到左子树不偷和偷左节点的最大金额
        right = self.traversal(node.right)  # 同上
        # 得到当前节点不偷和偷当前节点的最大金额
        val0 = max(left[0], left[1]) + max(right[0], right[1])  # 不偷当前节点
        val1 = node.val + left[0] + right[0]
        return (val0, val1)

思路2:递归+记录节点最大金额

由上面分析可知,遍历这棵树应当采用后序遍历。同时我们采用递归的方式。
还是根据是否偷当前节点来进行分析

  • 如果偷当前节点node
    那么左右孩子节点就不能偷,最大金额是node.val + (左子树不偷根节点最大金额) + (右子树不偷根节点最大金额)
    左子树不偷根节点最大金额(如果左节点存在) = function(node.left.left) + function(node.left.right)
  • 如果不偷当前节点node
    那么左右孩子节点就能偷,最大金额是function(node.left) + function(node.right)(左右子树最大金额之和,最大金额不一定偷了左右孩子节点)
    而分析上面发现,递归过程有许多重复的部分,因此我们采用map映射保存递归得到的以当前节点为根节点的树的最大金额。
    递归三部曲:
  • 参数和返回值:
    参数node,返回以node为根节点的树的最大金额
  • 边界条件:
    node为None,或者node为叶子节点,或者当前节点已经求过最大金额了,即map中存在当前节点对应的最大金额
  • 正常递归:
    就是前面是否偷当前节点的分析

代码:

# 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:
    memory = {}
    def rob(self, root: Optional[TreeNode]) -> int:
        # 边界
        if root is None:
            return 0
        if root.left is None and root.right is None:
            return root.val
        if self.memory.get(root) is not None:   # 之前求过了
            return self.memory[root]
        # 正常递归
        # 偷root节点
        val1 = root.val
        if root.left:
            val1 += self.rob(root.left.left) + self.rob(root.left.right)    # 不偷左节点的左子树最大金额
        if root.right:
            val1 += self.rob(root.right.left) + self.rob(root.right.right)
        # 不偷root节点
        val0 = self.rob(root.left) + self.rob(root.right)
        # 记录当前节点的最大金额
        self.memory[root] = max(val0, val1)
        return self.memory[root]

感悟:

根据题目分析应当采用树的什么遍历顺序,以及将当前节点当作根节点分析是否偷当前树,因为采用的递归,dp数组大小为2,保存偷当前节点和不偷当前节点的最大金额。(最后dp数组有点难得想到)


学习收获:

动态递归典型:打家劫舍基础 + 环状 + 树状

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值