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

代码随想录系列文章目录

动态规划篇 —— 打家劫舍


文章目录

  • 代码随想录系列文章目录
  • 198.打家劫舍
  • 213.打家劫舍II
  • 337.打家劫舍III(树形dp)
    • dfs+记搜
    • dp


今天总结打家劫舍问题

198.打家劫舍

题目链接

状态定义:dp[i]表示考虑到下标i为止,所能偷到的最高金额

状态转移:关键是考虑 偷不偷nums[i]
如果偷当前的nums[i]的话,就是dp[i-2]+nums[i], 如果不偷nums[i]的话就要考虑 dp[i-1]
所以 dp[i] = max(dp[i-2] + nums[i], dp[i-1])

base case: 根据递推公式就能发现 dp[0], dp[1]是需要先初始化的
dp[0] = nums[0], dp[1] = max(dp[0], dp[1])

遍历方向以及解的位置:
遍历方向就应该是正序,解的位置就是考虑到下标len(nums)-1 为止之前的所有情况的最大值

def rob(self, nums: List[int]) -> int:
        if len(nums) == 1: 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[len(nums)-1]

213.打家劫舍II

题目链接
这道题和上一道的区别就在于这道题的数组是成环的
成环的话我们需要适当对数组进行裁剪

第一种,考虑包含首元素,不包含尾元素
我们考虑的数组本身就不包含尾元素,不管我们选不选第一个,就都没问题了
代码随想录算法训练营第48天 | 198.打家劫舍 213.打家劫舍II 337.打家劫舍III_第1张图片
第二种,考虑包含尾元素,不包含首元素
代码随想录算法训练营第48天 | 198.打家劫舍 213.打家劫舍II 337.打家劫舍III_第2张图片
对数组进行裁剪之后,再对裁剪后的数组各自进行打家劫舍,运算的结果比较出一个最大的就行了

def rob(self, nums: List[int]) -> int:
        if len(nums) == 1: return nums[0]
        if len(nums) == 2: return max(nums[0], nums[1])
        
        def dprange(nums):
            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]
        
        res1, res2 = dprange(nums[1:]), dprange(nums[:-1])
        return max(res1, res2)  

337.打家劫舍III(树形dp)

题目链接
这个题还考察对二叉树的遍历,我发现我还是有点怕的,可能写的少吧

dfs+记搜

# 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: TreeNode) -> int:
        self.mem= {}
        def dfs(root):
            if not root:
                return 0
            if root in self.mem:
                return self.mem[root]
            ans1 = root.val     #选根和根孙子
            if root.left:
                ans1 += (dfs(root.left.left) + dfs(root.left.right))
            if root.right:
                ans1 += (dfs(root.right.left) + dfs(root.right.right))
            ans2 = dfs(root.left) + dfs(root.right)    #不选根  选根子树
            res = max(ans1, ans2)
            self.mem[root] = res
            return res
        return dfs(root)

时间复杂度:O(n),每个节点只遍历了一次
空间复杂度:O(log n),算上递推系统栈的空间

dp

动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。

1.确定递归函数参数以及返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。参数就是node或者命名cur

dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
所以本题dp数组就是一个长度为2的数组!

那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?
别忘了在递归的过程中,系统栈会保存每一层递归的参数。

2.确定终止条件也就是出口
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
这也相当于dp数组的初始化

if not node:
   return (0, 0)

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

left = dfs(node.left)
right = dfs(node.right)

4.确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就在回顾一下dp数组的含义)
为什么要cur.val + left[0] + right[0]呢? 我觉得因为是左右根的递归顺序,左边这个子树的状态,右边子树的状态算完,给根节点赋值

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

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

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

代码随想录算法训练营第48天 | 198.打家劫舍 213.打家劫舍II 337.打家劫舍III_第3张图片

# 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 not root: return 0
        if not root.left and not root.right: return root.val

        def dfs(node):
            if not node: return (0,0)
            left = dfs(node.left)
            right = dfs(node.right)

            #不偷当前结点,偷子树
            val0 = max(left[0], left[1]) + max(right[0], right[1])
            #偷当前结点, 不偷子结点
            val1 = node.val + left[0] + right[0]
            return (val0, val1)
        
        dp = dfs(root)
        return max(dp)

时间复杂度:O(n),每个节点只遍历了一次
空间复杂度:O(log n),算上递推系统栈的空间

你可能感兴趣的:(代码随想录算法训练营打卡,算法,leetcode,动态规划)