LeetCode 198. 打家劫舍
题目描述

题意分析
数组中每个非负整数表示一间房屋的金额,房屋按一条直线排列。选择若干间房屋,使任意两间被选中的房屋都不相邻,并让所选金额之和最大;返回最大总额,不需要返回选择方案。
不要求固定隔一间选一间,也不要求选够某个数量,必要时可以连续跳过多间。首尾房屋没有额外的环形相邻关系,是否冲突只看原数组中是否紧挨着。
解法:滚动变量动态规划
核心思路
[!blue]
处理一段房屋时,最后一间只有选或不选两种情况。令
f(i)表示只考虑下标0到i的房屋时能得到的最大金额,强调“考虑过”,并不要求一定选择第i间。如果不选第
i间,所有选择都位于前面的房屋,最佳收益就是f(i - 1)。如果选第i间,则第i - 1间不能选,而更前面的房屋只需满足原来的不相邻约束,最佳收益就是f(i - 2) + nums[i]。两种情况覆盖所有合法方案,分别取各自最优后再比较,得到f(i) = max(f(i - 1), f(i - 2) + nums[i])。转移只用到前两个状态,可以用
pre、cur替代整个数组。处理nums[i]之前,pre保存f(i - 2),cur保存f(i - 1);先用这两个旧状态计算next,再执行pre = cur、cur = next,它们就变成下一轮需要的前两个状态。初始化
pre = cur = 0,表示尚未考虑任何房屋时收益为零,也把第一间之前不存在的两个前缀统一当作空范围。第一间和第二间因此可以使用同一条转移,无需单独访问两个起始元素。每轮都保留选与不选的最优结果,避免根据眼前某一间金额作局部决定。
解题步骤
- 令
pre = 0、cur = 0,从左到右遍历每间房屋的金额num。- 计算
next = max(cur, pre + num):前者表示跳过当前房屋,后者表示选择当前房屋并避开前一间。- 保存好
next后,先将旧cur放入pre,再用next更新cur。- 遍历结束时,
cur已经表示整个数组的最优收益,返回它。
代码实现
class Solution {
public int rob(int[] nums) {
int pre = 0;
int cur = 0;
for (int num : nums) {
// 不选当前房屋沿用前一状态,选它则只能连接前两间的最优值。
int next = Math.max(cur, pre + num);
pre = cur;
cur = next;
}
return cur;
}
}
func rob(nums []int) int {
pre := 0
cur := 0
for _, num := range nums {
// 不选当前房屋沿用前一状态,选它则只能连接前两间的最优值。
next := cur
if pre+num > next {
next = pre + num
}
pre = cur
cur = next
}
return cur
}
复杂度分析
- 时间复杂度:$O(n)$,
n为房屋数量,每间房屋只做一次状态转移。- 空间复杂度:$O(1)$,只保留两个历史状态及本轮结果。
关键点总结
[!green]
- 前缀最优值不要求选择前缀末尾,才能完整表示“当前不选”的情况。
- 选择当前房屋时连接前两个位置的最优结果,确保不会同时选择相邻房屋。
- 先计算新值再滚动旧值,保持
pre、cur在每轮开始时的含义。- 最优方案不一定固定选择奇数位或偶数位,必须逐个前缀保留两种选择。
易错点总结
[!yellow]
- 先更新
pre再计算next,会把前一间之前的范围扩大一位,可能把相邻房屋的收益连在一起。- 只保留
pre + num,会强制选择当前房屋,丢掉跳过它可能更优的方案。- 把
cur理解为“一定偷上一间”,与实际转移的前缀最优定义不符。- 只比较相邻两间的金额或固定选择一组奇偶位置,不能覆盖所有合法的不相邻组合。
- 直接读取
nums[1]做初始化却未处理单间房屋,会越界;从两个零状态开始不需要这种分支。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 213. 打家劫舍 II | 中等 | 增加首尾相邻条件后,不能同时选择两端,需要拆成两条线性区间。 |
| 337. 打家劫舍 III | 中等 | 把线性相邻限制扩展到父子节点不能同时选择,状态仍是选与不选。 |
| 740. 删除并获得点数 | 中等 | 比较选择当前元素与跳过当前元素的最优值;本题禁止选择相邻房屋,该题按值累计收益后禁止选择相邻值。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!