目录

题目描述

213. 打家劫舍 II

image-20250419064937610

题意分析

一排房屋每间存有一定金额,nums[i] 就是第 i 间房的钱。要求选出若干间房把钱拿走,使总额最大。

限制只有一条:相邻的两间房不能同时被选,否则会触发报警。

与最基础的那道抢劫题相比,这里多了一个新约束——所有房屋围成一圈,第一间和最后一间也是相邻的。也就是说 nums[0]nums[n - 1] 同样不能同时入选,这是整道题唯一新增的信息。

边界方面要留意:n 可能等于 1。此时整个圈只有一间房,它和自己不构成相邻冲突,答案就是 nums[0]。金额均为非负数,所以「一间都不选」的方案价值为 0,永远不会比最优解更好。

解法:拆环后线性动态规划

核心思路

问题关键:普通打家劫舍只限制相邻房屋,环形版本额外让第一间和最后一间相邻。难点不在转移,而在防止首尾同时被选。

为什么拆成两个线性问题:任意合法方案至少满足“没选第一间”或“没选最后一间”。因此只需分别求区间 [1, n - 1][0, n - 2] 的线性最优值,再取较大者。两类允许在“首尾都不选”处重叠,因为求最大值不会重复累加。

在线性区间中,维护:

  • preOne:处理到前一间房时的最大金额;
  • preTwo:处理到前两间房时的最大金额。

当前房只有“不偷”和“偷”两种选择,转移为 cur = max(preOne, preTwo + nums[i])。这两个状态在每轮结束后继续向前滚动。

正确性:线性转移穷尽了当前房选或不选的全部合法情况,且选择当前房时只接在 i - 2 的最优方案后,不会出现相邻房。环形方案又必然属于上述至少一个区间;两个区间的方案都排除了首尾同选。因此二者最大值恰好是环上的最优解。

解题步骤

  1. 若只有一间房,直接返回其金额,避免拆出空区间。
  2. robRange(nums, left, right) 解决闭区间上的线性问题。
  3. 区间内从左到右计算 max(不偷当前房, 偷当前房),每轮更新两个历史状态。
  4. 分别计算“放弃最后一间”和“放弃第一间”,返回较大值。

面试口述示例[2,3,2] 拆成 [2,3][3,2],两个线性最优值都是 3,所以答案为 3;不能直接在线性数组上计算,否则可能把首尾两个 2 同时选入。

边界反例[5] 必须特判为 5;[2,1,1,2] 若忽略环会得到 4,而拆成两个区间后的答案是 3。

代码实现

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }

        int skipLast = robRange(nums, 0, n - 2);
        int skipFirst = robRange(nums, 1, n - 1);
        return Math.max(skipLast, skipFirst);
    }

    private int robRange(int[] nums, int left, int right) {
        int preTwo = 0;
        int preOne = 0;
        for (int i = left; i <= right; i++) {
            int cur = Math.max(preOne, preTwo + nums[i]);
            preTwo = preOne;
            preOne = cur;
        }
        return preOne;
    }
}
func rob(nums []int) int {
    n := len(nums)
    if n == 1 {
        return nums[0]
    }

    skipLast := robRange(nums, 0, n-2)
    skipFirst := robRange(nums, 1, n-1)
    if skipLast > skipFirst {
        return skipLast
    }
    return skipFirst
}

func robRange(nums []int, left int, right int) int {
    preTwo, preOne := 0, 0
    for i := left; i <= right; i++ {
        cur := preOne
        if preTwo+nums[i] > cur {
            cur = preTwo + nums[i]
        }
        preTwo, preOne = preOne, cur
    }
    return preOne
}

复杂度分析

  • 时间复杂度:$O(n)$,两个长度不超过 $n$ 的区间各扫描一次。
  • 空间复杂度:$O(1)$,线性 DP 只保留前两个状态。

关键点总结

  • 环比直线只多了首尾冲突;固定放弃其中一端,就能复用线性打家劫舍。
  • 两个区间不必互斥,只需覆盖全部合法方案;取最大值时重复覆盖不会影响答案。
  • preTwopreOne 始终对应更新前的 dp[i-2]dp[i-1],因此必须先算 cur 再滚动。
  • 若面试官要求一次 DP,也可以把“首房是否选择”加入状态,但两次线性扫描更短、更不易错。

易错点总结

  • 直接对整个数组做线性 DP:[2,1,1,2] 会非法地同时选择首尾,得到 4。
  • 漏掉 n == 1:两个拆分区间都会为空,[5] 会错成 0。
  • 区间边界仍同时包含首尾:[0, n - 1] 不是有效的拆环结果。
  • 滚动前先覆盖 preTwopreOne:当前转移会读取新值,等价于允许选择相邻房。
  • 把拆分误解成“必须恰好放弃一个端点”:首尾都不选同样合法,两个区间只是限制可选范围,并不强制选择端点。

相似题目

题目 难度 考察点
198. 打家劫舍 中等 线性数组上的相邻互斥基础 DP
337. 打家劫舍 III 中等 把相邻互斥搬到二叉树上做树形 DP
740. 删除并获得点数 中等 按值域重建数组后转化为相邻互斥
LCR 089. 打家劫舍 中等 同 198 的线性版本,可作滚动数组练手
LCR 090. 打家劫舍 II 中等 与本题同构的环形拆分
面试题 17.16. 按摩师 简单 相邻互斥的最简入门形式