研习代码 day41 | 动态规划——打家劫舍

一、打家劫舍(直线型)

        1.1 题目

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

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

示例 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.2 题目链接

        198.打家劫舍

        1.3 解题思路与过程想法

        (1)解题思路

        # 分析:前一位置状态决定后一状态,典型的动态规划问题
        # 边界情况:不满两户人家
        # 数组:0~i 内偷窃到的最大金额 dp[i],无负数且取最大,初始化可被覆盖的数据 0
                                dp = [0] * len(nums)
        # 递推关系:每个位置只有两种情况————隔一步跳 或 隔两步跳
                                dp[i] = max(dp[i-2]+nums[i], dp[i-1])
        # 初始化:由递推关系可知需初始化两个值
                                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])
        注意:最后结果不一定是最后位置,也可能是倒数第二个位置

        (2)过程想法

        从递推关系出发,代码是比较好写的

        1.4 代码

class Solution:
    def rob(self, nums: List[int]) -> int:
        # 分析:前一位置状态决定后一状态,典型的动态规划问题

        # 边界情况
        if len(nums) < 2:
            return nums[0]

        # 数组:0~i 内偷窃到的最大金额 dp[i],无负数且取最大,初始化可被覆盖的数据 0
        dp = [0] * len(nums)

        # 递推关系:每个位置只有两种情况————隔一步跳 或 隔两步跳
        # dp[i] = max(dp[i-2]+nums[i], dp[i-1])

        # 初始化:由递推关系可知需初始化两个值
        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 max(dp[len(nums)-2], dp[len(nums)-1])

二、打家劫舍 II (绕圈型)

        2.1 题目

        你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

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

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

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

示例 3:

输入:nums = [1,2,3]
输出:3

提示:

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

        2.2 题目链接

        213.打家劫舍 II

        2.3 解题思路与过程想法

        (1)解题思路

        # 分析:前一位置状态决定后一状态,典型的动态规划问题
        # 在原版打家劫舍的基础上围成一圈,不影响中间节点的处理,只是会影响首尾节点
        # 总共会有三种情况:首尾都不偷+不偷首+不偷尾,但其实后两种已经囊括了第一种
        # 需找到后两种情况中最优的,可直接利用其返回值,不再设 dp 数组

        # 边界情况:不满三户人家

        # 分析两种情况
                noFirst = self.steal(nums,1,len(nums)-1)
                noLast = self.steal(nums,0,len(nums)-2)
        # 找到后两种情况中最优的
                return max(noFirst,noLast)

        (2)过程想法

        最初想用 flag 来标记是否 steal 第一家,但是全程的配套处理有点复杂

        2.4 代码

class Solution:
    def rob(self, nums: List[int]) -> int:
        # 分析:前一位置状态决定后一状态,典型的动态规划问题
        # 在原版打家劫舍的基础上围成一圈,不影响中间节点的处理,只是会影响首尾节点
        # 总共会有三种情况:首尾都不偷+不偷首+不偷尾,但其实后两种已经囊括了第一种
        # 需找到后两种情况中最优的,可直接利用其返回值,不再设 dp 数组

        # 边界情况:不满三户人家
        if len(nums) < 3:
            return max(nums)

        noFirst = self.steal(nums,1,len(nums)-1)
        noLast = self.steal(nums,0,len(nums)-2)

        # 找到后两种情况中最优的
        return max(noFirst,noLast)

    def steal(self,nums,start,end) -> int:
        if start == end:
            return nums[start]

        preMax = nums[start]
        curMax = max(nums[start],nums[start+1])

        # 递推关系:每个位置只有两种情况————隔一步跳 或 隔两步跳
        for i in range(start+2,end+1):
            preMax,curMax = curMax,max(preMax+nums[i],curMax)

        return max(preMax,curMax)

三、打家劫舍 III (树形)

        3.1 题目

        小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

        除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

        给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:

研习代码 day41 | 动态规划——打家劫舍_第1张图片

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

研习代码 day41 | 动态规划——打家劫舍_第2张图片

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 10^4] 范围内
  • 0 <= Node.val <= 10^4

        3.2 题目链接

        337.打家劫舍 III

        3.3 解题思路与过程想法

        (1)解题思路

        # 分析:是否偷当前节点,需结合上一节点的情况综合考虑————动态规划问题
        # 数组 dp[0] 表示不偷当前节点,d[1] 表示偷当前节点
        # 遍历顺序:需根据孩子节点的情况,再决定偷根节点与否--->后续遍历(左右根)

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

        # 偷当前节点,不偷其孩子节点
                stealFather = root.val + left[0] + right[0]

        (2)过程想法

        树形DP不仅要掌握动态规划的知识,还要结合递归二叉树的知识,比较综合性

        3.4 代码

# 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[0] 表示不偷当前节点,d[1] 表示偷当前节点
        # 遍历顺序:需根据孩子节点的情况,再决定偷根节点与否--->后续遍历(左右根)
  
        dp = self.stealTraverse(root)

        # 选出最优方案
        return max(dp)

    def stealTraverse(self,root) -> int:
        # 递归出口
        if not root:
            return (0,0)

        left = self.stealTraverse(root.left)
        right = self.stealTraverse(root.right)

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

        # 偷当前节点,不偷其孩子节点
        stealFather = root.val + left[0] + right[0]

        return (stealChild,stealFather)

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