代码随想录算法训练营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)

你可能感兴趣的:(算法,动态规划,leetcode,数据结构,python)