← 返回题目列表

打家劫舍 II 的环形房屋如何拆成两个线性 DP?

高频 中等 第 4 / 33 题 更新于 2026/07/30
动态规划打家劫舍环形DP状态压缩

简化版

打家劫舍 II 比 I 多了首尾相邻限制。环形数组中第一间和最后一间不能同时偷,所以拆成两个线性问题:偷 [0..n-2] 或偷 [1..n-1],答案取两者最大。

详细版

线性打家劫舍的状态是:到当前房屋为止,最大收益为 max(prev2 + nums[i], prev1)。环形问题的关键是首尾互斥,如果偷第一间就不能偷最后一间,如果偷最后一间就不能偷第一间。

因此可以排除最后一间跑一次线性 DP,再排除第一间跑一次线性 DP。特殊情况 n=1 直接返回 nums[0]。时间复杂度 O(n),空间复杂度 O(1)

完整版教学

一、环形限制改变了什么

线性版本中,只有相邻房屋不能同时偷;环形版本里,nums[0]nums[n-1] 也相邻。

线性:0 - 1 - 2 - 3
环形:0 - 1 - 2 - 3
      └─────────┘

这个额外边会让普通线性 DP 可能选出非法答案。例如 [2,3,2],线性可能选 2 + 2 = 4,但首尾不能同时偷,正确答案是 3。

二、为什么可以拆成两个线性区间

首尾不能同时选,合法方案必然属于下面两类之一:

情况可考虑区间
不偷最后一间[0..n-2]
不偷第一间[1..n-1]

所有合法方案至少满足其中一个条件。即使某个方案首尾都不偷,它也会同时被两个区间覆盖,取最大不会漏答案。

记忆钩子:环形打家劫舍不是重新发明状态,而是切断首尾边。

三、线性子问题如何求

线性打家劫舍的转移是:

dp[i] = max(dp[i-1], dp[i-2] + nums[i])

含义是第 i 间房有两种选择:不偷它,收益沿用 dp[i-1];偷它,就不能偷 i-1,收益是 dp[i-2] + nums[i]

四、状态压缩写法

因为当前状态只依赖前一项和前两项,可以用两个变量:

prev2:处理到 i-2 的最大收益
prev1:处理到 i-1 的最大收益
cur = max(prev1, prev2 + nums[i])

这个 helper 可以复用两次,分别处理两个区间。

五、代码模板

int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];
    return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
}

int robRange(int[] nums, int l, int r) {
    int prev2 = 0;
    int prev1 = 0;
    for (int i = l; i <= r; i++) {
        int cur = Math.max(prev1, prev2 + nums[i]);
        prev2 = prev1;
        prev1 = cur;
    }
    return prev1;
}

注意 n=1 必须单独处理,否则两个区间会变成空或非法。

六、用样例验证拆分

[1,2,3,1] 为例:

区间线性答案说明
[0..2][1,2,3]4偷 1 和 3
[1..3][2,3,1]3偷 3

最终答案是 4。这个方案没有偷最后一间,所以合法。

七、常见误区与追问

  • 误区:直接套线性打家劫舍。 会可能同时选择首尾房屋,违反环形相邻限制。
  • 误区:只讨论偷第一间和偷最后一间。 更好写法是排除最后或排除第一,覆盖所有合法方案。
  • 误区:忘记处理 n=1。 单个房屋既是首也是尾,但没有另一间相邻房,答案就是它本身。
  • 追问:首尾都不偷的方案会不会漏? 不会,它同时包含在两个线性区间中。
  • 追问:复杂度是多少? 两次线性 DP,时间 O(n),空间 O(1)
  • 追问:如果是二叉树房屋呢? 那是打家劫舍 III,需要树形 DP,状态是偷或不偷当前节点。

八、加强记忆

打家劫舍 II 的关键动作是“把环切成两条线”。首尾相邻导致二者不能同时选,所以答案一定能在“不要最后一间”和“不要第一间”两个线性问题中找到。线性 helper 仍然是 max(偷当前, 不偷当前),只不过要跑两次。记住 [2,3,2] 这个反例,就不会误把环形题直接写成线性题。