当前位置: 首页 > news >正文

刷题记录 动态规划-29,30,31 HOT100 动态规划-3 打家劫舍系列

题目:

198. 打家劫舍

213. 打家劫舍 II

337. 打家劫舍 III

母题:198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

一、母题:打家劫舍

1.模式识别

当前房间偷不偷取决于上一个房间 》 当前状态取决于前面的状态 》 动态规划,当然打家劫舍本来就是经典动态规划题目

核心原理:当前状态 = max(打劫相邻房间, 不打劫相邻房间 + 当前房间金额)

2.母题代码实现

1.二维数组

维度1:房间

维度2:(打劫当前房间所得,不打劫当前房间所得)

即,当前状态 = max(上一个状态)

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [[0] * 2 for _ in range(n + 1)]
        for i, num in enumerate(nums):
            dp[i + 1][0] = max(dp[(i + 1) - 1])
            dp[i + 1][1] = dp[(i + 1) - 1][0] + num
        return max(dp[-1])

2.一维数组

当前状态 = max(上一个状态,上两个状态 + 当前节点值)

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [0] * (n + 2)
        for i, num in enumerate(nums):
            dp[i + 2] = max(dp[(i - 2) + 2] + num, dp[(i - 1) + 2])
        return dp[-1]

3.O1空间版

由于一维数组存在较强的无后效性,本题可以连数组都省去

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        prev1, prev2 = 0, 0
        cur = 0
        for num in nums:
            # 计算当前
            cur = max(prev1, prev2 + num)
            # 时间流逝
            prev1, prev2 = cur, prev1
        return cur

二、打家劫舍 + 循环数组

1.模式识别

当前状态 = max(打劫相邻房间, 不打劫相邻房间 + 当前房间金额)

对于类似数组这样的线性数据结构,可以通过分情况讨论来处理,

这里借用代码随想录的一段说明:代码随想录

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素

213.打家劫舍II

  • 情况二:考虑包含首元素,不包含尾元素

213.打家劫舍II1

  • 情况三:考虑包含尾元素,不包含首元素

213.打家劫舍II2

注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了

2.代码实现

1.二维数组

class Solution:
    def robOne(self, nums, start, end):
        n = len(nums)
        dp = [[0, 0] for _ in range(n + 2)]
        for i in range(start + 2, end + 3):
            dp[i][0] = max(dp[i - 1])
            dp[i][1] = dp[i - 1][0] + nums[i - 2]
        return max(dp[end + 2])

    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 1: return nums[0]
        ans1 = self.robOne(nums, 0, n - 2)
        ans2 = self.robOne(nums, 1, n - 1)
        return max(ans1, ans2)

2.一维数组

class Solution:
    def robOne(self, nums, start, end):
        n = len(nums)
        dp = [0] * (n + 2)
        for i in range(start, end + 1):
            dp[i + 2] = max(dp[(i - 1) + 2], dp[(i - 2) + 2] + nums[i])
        return dp[end + 2]
        
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 1: return nums[0]
        ans1 = self.robOne(nums, 0, n - 2)
        ans2 = self.robOne(nums, 1, n - 1)
        return max(ans1, ans2)

3.O1空间版

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 1: return nums[0]
        prev1, prev2 = 0, 0
        cur1 = 0
        for i in range(1, n):
            # 计算当前
            cur1 = max(prev1, prev2 + nums[i])
            # 时间流逝
            prev1, prev2 = cur1, prev1
        prev1, prev2 = 0, 0
        cur2 = 0
        for i in range(n - 1):
            # 计算当前
            cur2 = max(prev1, prev2 + nums[i])
            # 时间流逝
            prev1, prev2 = cur2, prev1
        return max(cur1, cur2)

三、打家劫舍 + 二叉树

1.模式识别

当前状态 = max(打劫相邻房间, 不打劫相邻房间 + 当前房间金额)

由于二叉树不是线性结构,因此不能直接套用母题模板,需要考虑遍历顺序:

本题前序、中序和层序都不可以,只有后序可以,具体原因后面详述

2.代码实现

1.二维数组

迭代:

# 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:
        dp0, dp1 = collections.defaultdict(int), collections.defaultdict(int)
        stk = [root]
        while stk:
            node = stk.pop()
            if node:
                stk.append(node)
                stk.append(None)
                if node.right: stk.append(node.right)
                if node.left: stk.append(node.left)
            else:
                node = stk.pop()
                dp1[node] = dp0[node.left] + dp0[node.right] + node.val
                dp0[node] = max(dp0[node.left], dp1[node.left]) + max(dp0[node.right], dp1[node.right])
        return max(dp0[root], dp1[root])

递归:

# 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:
        def helper(node):
            if not node: return 0, 0
            l0, l1 = helper(node.left)
            r0, r1 = helper(node.right)
            dp0 = max(l0, l1) + max(r0, r1)
            dp1 = l0 + r0 + node.val
            return dp0, dp1

        return max(helper(root))

2.一维数组

迭代:

# 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 = collections.defaultdict(int)
        stk, node, prev = [], root, None
        while stk or node:
            while node:
                stk.append(node)
                node = node.left
            node = stk.pop()
            if not node.right or node.right == prev:
                dp0 = dp[node.left] + dp[node.right]
                dp1 = node.val
                if node.left: dp1 += dp[node.left.left] + dp[node.left.right]
                if node.right: dp1 += dp[node.right.left] + dp[node.right.right]
                dp[node] = max(dp0, dp1)
                prev = node
                node = None
            else:
                stk.append(node)
                node = node.right
        return dp[root]

递归:(记忆化搜索)

# 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 not root: return 0
        if not root.left and not root.right: return root.val
        if root in self.memory: return self.memory[root]
        dp0 = self.rob(root.left) + self.rob(root.right)
        dp1 = root.val
        if root.left: dp1 += self.rob(root.left.left) + self.rob(root.left.right)
        if root.right: dp1 += self.rob(root.right.left) + self.rob(root.right.right)
        self.memory[root] = max(dp0, dp1)
        return self.memory[root]

3.O1空间版

二叉树不是线性结构,后序遍历有时候向上动,有时候向右动,但我估计能强行写出来,但太难写了,意义也不大

3.为什么只有后序可以?

注意打家劫舍的条件:当前状态 = max(打劫相邻房间, 不打劫相邻房间 + 当前房间金额)

即当前节点状态 = 打家劫舍函数(所有先前的相邻节点状态),

我们假设前序可以,从根节点开始,到根节点收集结果,将所有叶节点的结果算最大值

则可以写出这样的代码:

# 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:
        dummy1, dummy2 = TreeNode(0, root), TreeNode(0, root)
        parent, dp = {root: dummy1, dummy1: dummy2}, collections.defaultdict(int)
        ends = []
        stk = [root]
        while stk:
            node = stk.pop()
            dp0 = dp[parent[node]]
            dp1 = dp[parent[parent[node]]] + node.val
            dp[node] = max(dp0, dp1)
            print(node.val, dp0, dp1)
            if not node.left and not node.right: ends.append(node)
            if node.right: 
                stk.append(node.right)
                parent[node.right] = node
            if node.left: 
                stk.append(node.left)
                parent[node.left] = node
        return max(dp[end] for end in ends)

这样对吗?

以题干案例为例:

很显然是不对的,口算一下前序遍历的结果是6,而后序的结果也就是案例答案7,区别在哪里呢?

其实区别也很明显,前序遍历会忽略掉右下角的1

原因分析起来也很容易,也就是考虑相邻节点则前序遍历只能考虑到从上到下一条线上的结果,无法实现全局遍历

那如果将max(根节点结果)改为sum(根节点结果)行不行呢?

也不行,算一下就会知道,由于叶节点之间不相邻,所以这么做祖先节点会被重复计算

所以只有后序遍历才可以,因为

只有后序遍历才能同时实现遍历所有节点考虑相邻状态

相关文章:

  • Windows Docker玩转Nginx,从零配置到自定义欢迎页
  • HDFS分布式文件系统的架构及特点
  • DeepSeek-v1到DeepSeek-v3再到DeepSeek-R1的变迁和进化史,创新点,值得大家学习,DeepSeek系列干货
  • Windows之远程终端问题集锦(十二)
  • 基于Python+Vue开发的体育用品商城管理系统源码+开发文档+课程作业
  • SpringSecurity 实现token 认证
  • ExpMoveFreeHandles函数分析和备用空闲表的关系
  • IgH详解十八、支持 AoE 读写
  • 汽车小助手智能体
  • 6.7 数据库设计
  • 【FL0100】基于SSM微信小程序的走失人员的报备平台
  • rabbitmq单向ssl认证配置与最佳实践(适用于各大云厂商)
  • docker-compose Install MinerU 0.3 GPU模式
  • 大语言模型概念科普
  • Storm实时流式计算系统(全解)——中
  • Mixture of Experts与Meta Learning深度学习中的两大变革性技术
  • Android 图片压缩详解
  • 神经网络参数量计算
  • sql调优:优化响应时间(优化sql) ; 优化吞吐量
  • HumanPro逼真角色皮肤面部动画Blender插件V1.1版
  • 哪些网站是做包装的/代写文章哪里找写手
  • 官方网站制作思路/百度关键词分析工具
  • 个人建 行业 网站/网站建设的好公司
  • 做音响网站/微信营销的方法7种
  • 怎么做县城分类信息网站/青岛网络推广公司
  • 个人做网站多少钱/成都网站建设公司排名