环形房屋打家劫舍题解
1. 问题描述
所有房屋首尾相连形成环形排列 (第一个房屋和最后一个房屋相邻),相邻房屋装有联动防盗系统,若同时盗窃相邻两间房屋会触发警报。给定一个非负整数数组 nums,其中 nums[i] 表示第 i 间房屋存放的金额,要求计算在不触发警报的前提下,能盗窃到的最大金额。
2. 核心思路分析
本题的核心是动态规划的应用,原因如下:
- 问题具备重叠子问题 :计算第
i间房屋的最大可盗窃金额时,需要重复用到前i-1、i-2间房屋的计算结果; - 问题具备最优子结构:全局的最大金额可由每个位置的局部最优解推导而来。
而环形结构是本题的关键难点:由于首尾房屋相邻,"同时盗窃首尾"是非法的。因此我们可以将环形问题拆解为两个线性问题,规避首尾冲突:
- 情况1:不考虑最后一间房屋(只盗窃
[0, n-2]区间的房屋); - 情况2:不考虑第一间房屋(只盗窃
[1, n-1]区间的房屋)。
最终答案即为这两个线性问题的最大值------因为这两种情况已覆盖所有"不触发警报"的合法盗窃方式(要么不偷首,要么不偷尾,不可能同时偷首尾)。
对于线性房屋的打家劫舍问题,动态规划的状态转移方程为:
设 dp[i] 表示盗窃到第 i 间房屋时能获得的最大金额,则:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
dp[i-1]:不偷第i间房屋,最大金额等于前i-1间的最优解;dp[i-2] + nums[i]:偷第i间房屋,此时第i-1间不能偷,最大金额等于前i-2间的最优解加上第i间的金额。
此外,我们可以对空间进行优化:无需维护完整的 dp 数组,只需用两个变量 pre(表示 dp[i-2])和 cur(表示 dp[i-1])滚动更新,将空间复杂度从 O(n) 降至 O(1)。
3. 解题步骤
- 边界条件处理 :
- 若数组为空(
n=0),返回 0; - 若数组只有1间房屋(
n=1),直接返回该房屋的金额(无相邻冲突)。
- 若数组为空(
- 定义辅助函数 :实现线性区间
[start, end]内的最大盗窃金额计算逻辑:- 初始化
pre=0(前前一个位置的最优解)、cur=0(前一个位置的最优解); - 遍历区间内的每间房屋,通过滚动变量更新当前最优解;
- 初始化
- 拆分环形问题 :
- 计算
[0, n-2]区间的最大金额(不偷最后一间); - 计算
[1, n-1]区间的最大金额(不偷第一间);
- 计算
- 返回结果:取上述两个结果的最大值。
4. 代码实现
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
// 边界条件1:无房屋可偷
if (n == 0) return 0;
// 边界条件2:只有1间房屋,直接偷
if (n == 1) return nums[0];
// 拆分为两个线性问题,取最大值
int res1 = robLinear(nums, 0, n-2); // 不偷最后一间
int res2 = robLinear(nums, 1, n-1); // 不偷第一间
return max(res1, res2);
}
// 辅助函数:计算线性区间[start, end]内的最大盗窃金额
int robLinear(vector<int>& nums, int start, int end) {
int pre = 0; // 代表dp[i-2],前前一个位置的最优解
int cur = 0; // 代表dp[i-1],前一个位置的最优解
for (int i = start; i <= end; ++i) {
int temp = cur; // 暂存当前cur(更新前的dp[i-1])
// 更新cur为当前位置i的最优解:max(不偷i, 偷i)
cur = max(pre + nums[i], cur);
pre = temp; // pre更新为原来的cur(即新的dp[i-1])
}
return cur; // 最终cur为区间[start, end]的最优解
}
};
5. 复杂度分析
- 时间复杂度 :
O(n)。仅需两次线性遍历数组([0,n-2]和[1,n-1]),每次遍历的时间为O(n),整体仍为O(n); - 空间复杂度 :
O(1)。仅使用了常数个临时变量(pre、cur、temp等),未额外开辟与数组长度相关的空间。
总结
- 环形房屋打家劫舍的核心是拆解问题:将环形转化为"不偷首"或"不偷尾"的两个线性问题,规避首尾相邻的冲突;
- 线性问题的最优解通过动态规划+空间优化实现,用两个变量滚动更新,兼顾时间和空间效率;
- 边界条件需特殊处理(空数组、单元素数组),避免逻辑漏洞。