代码随想录算法训练营day48 | 198.打家劫舍,213.打家劫舍II,337.打家劫舍III

198.打家劫舍

动规五部曲:

  1. dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
  2. 确定递推公式:决定dp[i]的因素就是第i房间偷还是不偷, dp[i] = max(dp[i-2]+nums[i], dp[i-1])
  3. dp数组如何初始化: 从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]。从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
  4. 确定遍历顺序:dp[i] 是根据dp[i - 2]和dp[i - 1]推导出来的,那么一定是从前到后遍历!
  5. 打印检查
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]
        
        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]

213.打家劫舍II

难点在于成环后需要考虑的情况:

  1. 不包含首尾元素
  2. 包含首元素,不包含尾元素
  3. 包含尾元素,不包含首元素

注意:情况2和3实际上是包含情况1的,因此需要做的是基于#198这道题,分别对于两种情况进行求解,最终取最大值

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.robBase(nums, 0, len(nums)-2)
        res2 = self.robBase(nums, 1, len(nums)-1)
        return max(res1, res2)


    def robBase(self, nums, start, end):
        if start == end:
            return nums[start]
        dp = [0]*len(nums)
        dp[start] = nums[start]
        dp[start+1] = max(nums[start], nums[start+1])
        for i in range(start+2, end+1):
            dp[i] = max(dp[i-2]+nums[i], dp[i-1])
        return dp[end]
        

代码难点:

  • 注意将#198逻辑进行抽象,如何利用start和end来控制数组

337.打家劫舍III

思路:

  • 树的遍历方式:前中后序遍历(深度优先搜索DFS)和层序遍历(广度优先搜索BFS)
  • 与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”)

方法1: 暴力递归 --> Time Limit Exceeded

# 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):
    def rob(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        if root is None:
            return 0
        if root.left is None and root.right is None:
            return root.val
        
        #偷父节点: 两个孩子就不能动,要考虑孙子
        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)
        #不偷父节点: 则考虑两个孩子
        val2 = self.rob(root.left) + self.rob(root.right)
        return max(val1, val2)

注意:

这个递归的过程中其实是有重复计算了。在计算root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,此时计算左右孩子的时候其实又把孙子计算了一遍。因此出现大量重复计算,这也是记忆化递归的用武之地。

方法2: 记忆化递归

什么是记忆化递归?

  • 使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。(就是算过的存下来直接用)
  • 如果有结果,就直接拿来用;在最终返回前将结果保存到map里
# 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):
    memory = {}
    def rob(self, root):
        """
        :type root: TreeNode
        :rtype: 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): #有结果就直接拿来用
            return self.memory[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)
        #不偷父节点: 则考虑两个孩子
        val2 = self.rob(root.left) + self.rob(root.right)
        self.memory[root] = max(val1, val2) #将结果保存到map里
        return max(val1, val2)

方法3: 动态规划

  • 在上面两种方法,其实对一个节点偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
  • 树形dp,因此递归三部曲 + 动规五部曲

递归三部曲​​​​​​​1. 确定递归函数的参数和返回值

  • 要求一个节点偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组,参数为当前节点。
  • 结合五部曲来看,返回的其实就是dp数组;这样dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。所以本题dp数组就是一个长度为2的数组,因为在递归的过程中,系统栈会保存每一层递归的参数。(个人理解就是不需要像之前的动规题那样维护一个较长的数组,递归允许只维护两个状态且动态更新)

递归三部曲​​​​​​​2. 确定终止条件:

  • 在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回。
  • 结合五部曲来看,相当于dp数组的初始化

确定遍历顺序

  • 首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。
  • 通过递归左节点,得到左节点偷与不偷的金钱。
  • 通过递归右节点,得到右节点偷与不偷的金钱。

递归三部曲​​​​​​​3. 确定单层递归的逻辑:

也是确定dp[i]的递推公式:

  • 如果是当前节点,那么左右孩子就不能偷,val1 = cur.val + left[0] + right[0](参考第一步dp数组含义,下标为0记录不偷该节点所得到的的最大金钱);
  • 如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]); 难道left[0]还会比left[1]大?最后当前节点的状态就是{val2, 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:
        # dp数组(dp table)以及下标的含义:
        # 1. 下标为 0 记录 **不偷该节点** 所得到的的最大金钱
        # 2. 下标为 1 记录 **偷该节点** 所得到的的最大金钱
        dp = self.traversal(root)
        return max(dp)

    def traversal(self, node):
        
        # 递归终止条件,就是遇到了空节点,那肯定是不偷的
        if not node:
            return (0, 0)

        left = self.traversal(node.left)
        right = self.traversal(node.right)

        # 不偷当前节点, 偷子节点
        val_0 = max(left[0], left[1]) + max(right[0], right[1])

        # 偷当前节点, 不偷子节点
        val_1 = node.val + left[0] + right[0]

        return (val_0, val_1)

代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值