题目描述

✅ LCR 090. 打家劫舍 II

image-20260929004147394

image-20260929004147395

题意分析

房屋围成一圈,相邻两间不能同时选择,第一间和最后一间也相邻。要求在这个限制下取得最大金额。

当房屋至少有两间时,任意合法方案都至少放弃首尾中的一间。因此可以分别在“排除最后一间”和“排除第一间”的两个线性区间中求最优,再取较大值。

解法:拆成两段线性打家劫舍

核心思路

[!blue]

分别计算 [0, n-2] 和 [1, n-1]。任何环上的合法方案都包含在至少一个区间的方案集合里;反过来,每个区间都缺少一端,在线性相邻限制下得到的方案也一定不会违反首尾互斥。因此两个最优值取最大,恰好覆盖环上的最优方案。

两次计算都不需要强制选择另一端。首尾都不选的方案可能出现在两个区间中,但重复参与取最大值不会改变答案。

在线性区间里,每轮开始时,f 表示上一间不选时的最优前缀收益,g 表示上一间被选时的最优前缀收益。处理当前金额 x,不选当前可以承接旧的任意状态,所以新 f = max(f, g);选择当前必须承接上一间未选的状态,所以新 g = f + x。

两个候选分别覆盖当前选与不选的全部可能,每一步都保持相邻互斥,因此扫描完后 max(f, g) 就是这段区间的最优值。变量初始都置零用于启动递推;处理第一间后得到“不选为零、选择为其金额”,此后才对应上述真实前缀的两种状态,不必把初始 g 理解为已经偷了不存在的房屋。

更新两个变量时必须使用旧状态。Java 先把新的 f 暂存为 ff,再用旧 f 计算 g;Go 的并行赋值会先计算所有右侧表达式,再统一赋值。

n == 1 时首尾是同一间房,不存在两间之间的冲突,直接返回它的金额。n == 2 时两个区间各含一间房,两次计算自然得到两者的较大金额。

解题步骤

  1. 只有一间房时直接返回其金额。
  2. 分别在线性区间 [0, n-2]、[1, n-1] 上调用子过程。
  3. 子过程用旧的 f、g 同时计算当前不选和选择的最优值,最终返回两者最大值。
  4. 两个区间的答案取最大值,作为环形问题的结果。

代码实现

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;

        // 只有一间房时首尾是同一间,拆区间会得到空区间,必须特判。
        if (n == 1) {
            return nums[0];
        }

        // 左:放弃最后一间;右:放弃第一间。两者覆盖全部合法方案。
        return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
    }

    // 在线性区间 [l, r] 上做普通打家劫舍。
    // f:上一间不偷时的最优值;g:上一间偷了时的最优值。
    private int rob(int[] nums, int l, int r) {
        int f = 0;
        int g = 0;

        for (; l <= r; ++l) {
            // 先把新的 f 算出来暂存,右式里的 f 必须还是旧值。
            int ff = Math.max(f, g);

            g = f + nums[l];
            f = ff;
        }

        return Math.max(f, g);
    }
}
func rob(nums []int) int {
    n := len(nums)
    // 只有一间房时首尾是同一间,拆区间会得到空区间,必须特判。
    if n == 1 {
        return nums[0]
    }
    // 左:放弃最后一间;右:放弃第一间。两者覆盖全部合法方案。
    return max(robRange(nums, 0, n-2), robRange(nums, 1, n-1))
}

// 在线性区间 [l, r] 上做普通打家劫舍。
// f:上一间不偷时的最优值;g:上一间偷了时的最优值。
func robRange(nums []int, l, r int) int {
    f, g := 0, 0
    for _, x := range nums[l : r+1] {
        // 并行赋值:右侧全部用旧值求值,天然避免了顺序陷阱。
        f, g = max(f, g), f+x
    }
    return max(f, g)
}

复杂度分析

  • 时间复杂度:$O(n)$,两个长度为 n-1 的区间各扫描一次。
  • 空间复杂度:$O(1)$,区间通过下标访问,滚动状态只有常数个变量;Go 的切片视图也不会复制原数组。

关键点总结

[!green]

  • 拆分条件是“排除一端”,不是“必须选择另一端”,两组方案合起来覆盖全部合法选择。
  • 两个区间有重叠不影响取最大值,单间房则必须在拆分之前单独处理。
  • 当前选择状态只能由旧的不选状态转移,先覆盖旧值会错误允许相邻两间同时被选。

易错点总结

[!yellow]

  • 环上首尾相邻,分别排除首家或尾家做两次线性问题;不必强制选择某一端。
  • 单家先特判,避免拆成两个空区间。
  • 滚动更新时保留旧的不偷状态,不能把刚更新的值拿去计算新偷状态。

相似题目

题目 难度 关联与区别
198. 打家劫舍 中等 本题通过分别排除首项和末项,转化为两次线性打家劫舍。
740. 删除并获得点数 中等 同样化为相邻类别不能同时选择,原题先按数值聚合收益,本题邻接来自环形位置。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/73555304
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!