Leetcode刷题笔记48:动态规划9(198.打家劫舍-213.打家劫舍II-337.打家劫舍 III)

107 阅读2分钟

导语

leetcode刷题笔记记录,本篇博客是动态规划部分,主要记录题目包括:

Leetcode 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

解法

读完题目后可以发现,数组第i个位置的值其实只与前2个元素是否被偷有关。使用动规五部曲:

  1. dp数组含义:dp[i]表示截止到第i个下标的位置,所偷取的最大物品价值为dp[i];
  2. 递推公式,当前dp[i]的取值分为两种情况:
    • 偷i:那么dp[i-2] + nums[i]
    • 不偷i:那么dp[i-1](这里并不一定代表i-1被选中)

所以,得到递推公式为:dp[i]=max(dp[i2]+nums[i],dp[i1])dp[i]=max(dp[i-2]+nums[i],dp[i-1])

  1. 初始化,只需要确定dp[0]和dp[1]的初始值就可以,因为后面的值全部由这两个值推导过来,考虑dp数组含义,那么dp[0]=nums[0],dp[1]=max(nums[0],nums[1])dp[0]=nums[0],dp[1]=max(nums[0],nums[1]),其余初识为0(任意值都行,不影响)
  2. 遍历顺序,从前往后;
  3. 打印dp数组:略
from typing import List

class Solution:
    def rob(self, nums: List[int]) -> int:
        # 如果只有一个或零个房子,直接返回这个房子的金额(或0)。
        if len(nums) <= 1:
            return nums[0]
        
        # 初始化动态规划数组 dp,长度为 len(nums)。
        # dp[i] 表示在考虑前 i+1 个房子时能偷窃到的最高金额。
        dp = [0] * len(nums)
        
        # 边界条件:第一个和第二个房子的最高金额是它们自己的金额和它们中较大的那个。
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        
        # 循环遍历所有其他房子。
        for i in range(2, len(nums)):
            # 对于每一个房子,我们有两个选择:
            # 1) 偷窃这个房子,并加上 dp[i-2](因为不能偷窃相邻的房子)。
            # 2) 不偷窃这个房子,那么最大金额就是 dp[i-1]。
            # 我们取这两个选项中的较大值。
            dp[i] = max(dp[i-2] + nums[i], dp[i-1])
        
        # 返回考虑所有房子后能偷窃到的最高金额。
        return dp[len(nums) - 1]

Leetcode 213.打家劫舍II

题目描述

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

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

 

示例 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

解法

我们可以将这个环形的问题转化为两个线性的问题,既然首尾元素不能同时取到,那么我们考虑两种情况:

  1. 不包含首元素的线性列表;
  2. 不包含尾元素的线性列表;

最后,只需要返回两种情况下的最大值即可,完整代码如下:

from typing import List

class Solution:
    def rob(self, nums: List[int]) -> int:
        # 如果只有一个或零个房子,直接返回这个房子的金额(或0)
        if len(nums) <= 1:
            return nums[0]
        
        # 分别去掉第一个和最后一个元素,形成两个新数组
        # 因为第一个和最后一个房子不能同时被偷窃
        nums1, nums2 = nums[:-1], nums[1:]
        
        # 使用 rob_linear 方法计算每个数组能偷窃到的最高金额,并取两者之间的最大值
        return max(self.rob_linear(nums1), self.rob_linear(nums2))

    def rob_linear(self, nums: List[int]) -> int:
        # 如果只有一个或零个房子,直接返回这个房子的金额(或0)
        if len(nums) <= 1:
            return nums[0]
        
        # 初始化动态规划数组 dp,长度为 len(nums)
        # dp[i] 表示在考虑前 i+1 个房子时能偷窃到的最高金额
        dp = [0] * len(nums)
        
        # 边界条件:第一个和第二个房子的最高金额是它们自己的金额和它们中较大的那个
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        
        # 循环遍历所有其他房子
        for i in range(2, len(nums)):
            # 对于每一个房子,我们有两个选择:
            # 1) 偷窃这个房子,并加上 dp[i-2](因为不能偷窃相邻的房子)
            # 2) 不偷窃这个房子,那么最大金额就是 dp[i-1]
            # 我们取这两个选项中的较大值
            dp[i] = max(dp[i-2] + nums[i], dp[i-1])
        
        # 返回考虑所有房子后能偷窃到的最高金额
        return dp[len(nums) - 1]

Leetcode 337.打家劫舍 III

题目描述

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

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

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

 

示例 1:

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

示例 2:

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

 

提示:

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

解法

使用递归三部曲,后续遍历二叉树。每当它访问一个节点时,它都计算两个值:一个是如果不偷该节点可获得的最大金额(val1),另一个是如果偷该节点可获得的最大金额(val2)。

  • val1 是左右子树可偷窃的最大金额之和。
  • val2 是包括当前节点在内的可偷窃的最大金额。

这两个值都保存在一个列表中,递归地返回给父节点,父节点再根据这些值来更新自己的 val1val2

最终的答案是根节点的 val1val2 中的较大值,即 max(self.rob_tree(root))

from typing import Optional

class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        # 调用 rob_tree 函数并取得可以偷窃的最大金额
        return max(self.rob_tree(root))

    def rob_tree(self, cur):
        # 如果当前节点为空,则返回 [0, 0],表示不偷或偷都是0
        if cur is None:
            return [0, 0]

        # 对左子树进行递归调用
        left = self.rob_tree(cur.left)
        # 对右子树进行递归调用
        right = self.rob_tree(cur.right)

        # 不偷当前节点,则最大金额是左右子树可偷窃的最大金额之和
        # left[0] 和 left[1] 分别表示不偷和偷左子树能得到的最大金额
        # right[0] 和 right[1] 同理
        val1 = max(left[0], left[1]) + max(right[0], right[1])

        # 如果偷当前节点,则左右子树都不能偷,所以只能加上左右子树不偷时的最大金额
        val2 = cur.val + left[0] + right[0]

        # 返回一个列表,第一个元素是不偷当前节点可得到的最大金额,第二个元素是偷当前节点可得到的最大金额
        return [val1, val2]