跳到主要内容

BM79. 打家劫舍(二)

题目描述​

你是一个经验丰富的小偷,准备偷沿湖的一排房间。这些房间组成一个闭合的圆环, 即第一个房间和最后一个房间视为相邻。仍然不能偷相邻的两家。 给定长度为 n 的整数数组 nums,请你计算在不被发现的前提下最多的偷窃金额。

输入格式​

  • nums:int 型一维数组,表示每个房间的现金数额。

输出格式​

返回 int 型,最多能偷到的金额。

示例​

示例 1​

输入:

nums = [1,2,3,4]

输出:

6

说明: 最优方案是偷第 2、4 个房间,金额 2 + 4 = 6。

示例 2​

输入:

nums = [1,3,6]

输出:

6

说明: 由于 1 和 6 相邻,最优方案是只偷第 3 个房间,金额 6。


解题思路​

第一步:理解问题本质​

和「打家劫舍(一)」相比,唯一的区别是房间排成了一个环: 第一个房间和最后一个房间也相邻。于是多了一条约束 —— 首尾不能同时偷。

第二步:把环拆成两条直线​

既然约束只有「首尾不能同时偷」,就可以分情况讨论:

  • 情况 A:不偷最后一个房间 → 问题等价于在 nums[0 .. n-2] 上做线性打家劫舍;
  • 情况 B:不偷第一个房间 → 问题等价于在 nums[1 .. n-1] 上做线性打家劫舍。

这两种情况已经覆盖了所有合法方案(任何合法方案要么不偷最后一个, 要么不偷第一个)。答案就是两者取最大值。

第三步:复用线性解法​

线性打家劫舍就是 BM78 的滚动写法。把它抽成一个辅助函数 _rob_line, 环形版本调用两次即可,代码干净且不易写错。

注意 n == 1 时只有一个房间,「首尾相邻」退化成自己,直接返回 nums[0]。


完整代码实现​

算法可视化 · 逐帧回放这道题的代码9 帧 · 录制输入 [1,2,3,4]
from typing import List


class Solution:
def rob(self, nums: List[int]) -> int:
"""环形打家劫舍:拆成两个线性子问题取最大值"""
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
# 方案一:不考虑最后一个房间;方案二:不考虑第一个房间
return max(self._rob_line(nums[:-1]), self._rob_line(nums[1:]))

def _rob_line(self, arr: List[int]) -> int:
"""线性打家劫舍(辅助方法,下划线开头)"""
prev, cur = 0, 0
for x in arr:
prev, cur = cur, max(cur, prev + x)
return cur

示例推演​

以 nums = [1,2,3,4] 为例:

方案子数组线性最优解说明
A:不偷最后一个[1,2,3]max(1+3, 2) = 4偷第 1、3 个
B:不偷第一个[2,3,4]max(2+4, 3) = 6偷第 2、4 个

取最大值 6,与答案一致。

再以 nums = [1,3,6] 为例:

方案子数组线性最优解
A:不偷最后一个[1,3]max(1, 3) = 3
B:不偷第一个[3,6]max(3, 6) = 6

取最大值 6,与答案一致。


复杂度分析​

解法时间复杂度空间复杂度说明
拆成两个线性问题O(n)O(1)推荐

两次线性扫描合起来仍是 O(n)。


易错点总结​

1. n == 1 的边界​

只有一个房间时,nums[:-1] 和 nums[1:] 都是空数组, 会错误地返回 0。必须特判直接返回 nums[0]。

2. 用「首尾都偷」的贪心​

环形的关键就是首尾互斥,不能简单地在原数组上套线性 DP。

3. 只拆一种情况​

只去掉首或只去掉尾都不够,必须两种情况取最大。


扩展思考​

  • 若环被「打断」成两条链(给定某条边不能走),就退化成两个线性问题。
  • 更一般的「树上不能偷相邻节点」用树形 DP,思路同样是把选择传给父节点。

相关题目​

自测​

加载评论中...