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 说明
- 来源: 力扣(LeetCode)
- 链接: https://leetcode-cn.com/problems/house-robber-ii
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 行返回 cur
和 nxt
的效果都是一样的(因为在返回前的 for
循环会进行赋值操作 prev, cur = cur, nxt
),但是问题在于,当 nums
的长度为 3 3 3 时,压根不会进行 for
循环,此时由于 cur
和 nxt
的初始值不同,所以必须返回 cur
,因此为了不失一般性,需要返回 cur
作为最终结果。