【LeetCode 动态规划专项】打家劫舍II(213)

1. 题目

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

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

1.1 示例

  • 示例 1 1 1
  • 输入: nums = [2, 3, 2]
  • 输出: 3 3 3
  • 解释: 你不能先偷窃 1 1 1 号房屋(金额 = 2 = 2 =2),然后偷窃 3 3 3 号房屋(金额 = 2 = 2 =2), 因为他们是相邻的。
  • 示例 2 2 2
  • 输入: nums = [1, 2, 3, 1]
  • 输出: 4 4 4
  • 解释: 你可以先偷窃 1 1 1 号房屋(金额 = 1 = 1 =1),然后偷窃 3 3 3 号房屋(金额 = 3 = 3 =3)。偷窃到的最高金额 = 1 + 3 = 4 = 1 + 3 = 4 =1+3=4
  • 示例 3 3 3
  • 输入: nums = [0]
  • 输出: 0 0 0

1.2 说明

1.3 提示

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

1.4 进阶

你可以继续求解出如何行窃才能获得最大的金额么?

2. 解法一(自底向上动态规划)

2.1 分析

本题是【LeetCode 动态规划专项】打家劫舍(198)的简单拓展,由于第一个房屋和最后一个房屋是紧挨着的,因此需要保证第一间房屋和最后一间房屋不同时偷窃。

如何才能保证第一间房屋和最后一间房屋不同时偷窃呢?如果偷窃了第一间房屋,则不能偷窃最后一间房屋,因此偷窃房屋的范围是第一间房屋到倒数第二间房屋;如果偷窃了最后一间房屋,则不能偷窃第一间房屋,因此偷窃房屋的范围是第二间房屋到最后一间房屋。

假设数组 nums 的长度为 n。如果不偷窃最后一间房屋,则偷窃房屋的下标范围是 [0, n - 2];如果不偷窃第一间房屋,则偷窃房屋的下标范围是 [1, n − 1]

2.2 解答

from typing import List


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

    def rob(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return nums[0]
        if len(nums) == 2:
            return max(nums[0], nums[1])
        dp = [0 for _ in range(len(nums) - 1)]
        return max(self._iterative_rob(nums, dp), self._iterative_rob(list(reversed(nums)), dp))


def main():
    nums = [1, 2, 3, 1, 1, 2, 3, 6]
    sln = Solution()
    print(sln.rob(nums))  # 11


if __name__ == '__main__':
    main()

2.3 复杂度

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组长度。
  • 空间复杂度: O ( n ) O(n) O(n)

实际上,上述代码还可以通过以下更直观的方式给出解答:

from typing import List


class Solution:
    def _intuitive_iterative_rob(self, nums: List[int], dp: List[int]) -> int:
        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]

    def intuitive_rob(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return nums[0]
        if len(nums) == 2:
            return max(nums[0], nums[1])
        dp = [0 for _ in range(len(nums) - 1)]
        return max(self._intuitive_iterative_rob(nums[1:], dp),
                   self._intuitive_iterative_rob(nums[:len(nums) - 1], dp))


def main():
    nums = [1, 2, 3, 1, 1, 2, 3, 6]
    sln = Solution()
    print(sln.rob(nums))  # 11
    print(sln.intuitive_rob(nums))  # 11


if __name__ == '__main__':
    main()

实际上,和【LeetCode 动态规划专项】打家劫舍(198)一样,还可以进一步将该问题的空间复杂度降低至 O ( 1 ) O(1) O(1)

from typing import List


class Solution:
    def _efficient_rob(self, nums: List[int], start: int, stop: int) -> int:
        prev, cur, nxt = nums[start], max(nums[start], nums[start + 1]), 0
        for i in range(start + 2, stop):
            nxt = max(prev + nums[i], cur)
            prev, cur = cur, nxt
        return cur

    def space_efficient_rob(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return nums[0]
        if len(nums) == 2:
            return max(nums[0], nums[1])
        return max(self._efficient_rob(nums, 0, len(nums) - 1),
                   self._efficient_rob(nums, 1, len(nums)))


def main():
    nums = [1, 2, 3, 1, 1, 2, 3, 6]
    sln = Solution()
    print(sln.space_efficient_rob(nums))


if __name__ == '__main__':
    main()

对于上述代码,需要注意的是:虽然在一般情况下在第 10 10 10 行返回 curnxt 的效果都是一样的(因为在返回前的 for 循环会进行赋值操作 prev, cur = cur, nxt),但是问题在于,当 nums 的长度为 3 3 3 时,压根不会进行 for 循环,此时由于 curnxt 的初始值不同,所以必须返回 cur ,因此为了不失一般性,需要返回 cur 作为最终结果。

猜你喜欢

转载自blog.csdn.net/weixin_37780776/article/details/120680072