📌 题目链接:198. 打家劫舍 - 力扣(LeetCode) 🔍 难度:中等 | 🏷️ 标签:数组、动态规划
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
🧠 题目分析
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,唯一限制是不能连续偷两家相邻的房子,否则会触发警报。
给定一个非负整数数组 nums,其中 nums[i] 表示第 i 间房子中的金额,求在不触发警报的前提下,你能偷到的最大金额。
这个问题本质上是一个最优化问题,具有明显的最优子结构和重叠子问题特性,非常适合使用 动态规划(Dynamic Programming, DP) 来解决。
🔑 核心算法及代码讲解
✨ 动态规划思想详解(线性DP)
本题属于经典的 “打家劫舍”系列问题 的第一题,是面试中非常高频的动态规划入门题。
📌 为什么能用动态规划?
- 最优子结构:全局最优解包含局部最优解。比如,前 i 间房子能偷的最大金额,依赖于前 i−1 和 i−2 间房子的最大金额。
- 无后效性:当前状态只与前两个状态有关,不会受更早状态的影响。
- 重叠子问题:多个子问题会被重复计算(如 dp[3] 会用到 dp[1],dp[4] 也会用到 dp[2] → dp[1]),适合用 DP 缓存结果。
📌 状态定义
设 dp[i] 表示 从前 i+1 间房子(即索引 0 到 i)中能偷到的最大金额。
📌 状态转移方程
对于第 i 间房子(i ≥ 2),有两种选择:
- 偷第 i 间:那么就不能偷第
i−1间,总金额为dp[i−2] + nums[i] - 不偷第 i 间:那么最大金额就是
dp[i−1]
取两者最大值:
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
📌 边界条件
dp[0] = nums[0](只有一间房,只能偷它)dp[1] = max(nums[0], nums[1])(两间房,选金额大的)
📌 空间优化:滚动数组
注意到 dp[i] 只依赖 dp[i−1] 和 dp[i−2],因此无需维护整个 dp 数组,只需两个变量即可:
first:代表dp[i−2]second:代表dp[i−1]
每次迭代更新这两个变量,空间复杂度从 O(n) 降至 O(1) ,这是面试中非常加分的优化点!
🧩 解题思路(分步拆解)
-
处理边界情况:
- 如果数组为空,返回 0;
- 如果只有一间房,直接返回
nums[0]。
-
初始化前两个状态:
first = nums[0]second = max(nums[0], nums[1])
-
从第 3 间房开始遍历(i = 2 到 n−1) :
- 计算当前最大金额:
max(first + nums[i], second) - 更新
first = second,second = 当前最大金额
- 计算当前最大金额:
-
返回最终结果:
second即为答案。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n):仅需一次遍历数组 |
| 空间复杂度 | O(1):仅使用常数个额外变量 |
| 是否可扩展 | ✅ 是!后续的“环形打家劫舍”(House Robber II)、“树形打家劫舍”(House Robber III)均基于此思想扩展 |
| 面试高频点 | ✔️ 状态转移方程推导 ✔️ 滚动数组优化 ✔️ 边界条件处理 ✔️ 与贪心的区别(贪心在此不适用!) |
💡 常见误区提醒:
有人会误以为“每次选最大的不相邻数”就行(贪心),但反例[2,1,1,2]中,贪心选 2+2=4 正确,但若为[5,1,1,5,1,1,5],贪心可能选错。必须用 DP 考虑全局最优。
💻 代码
C++ 实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int rob(vector<int>& nums) {
// 边界:空数组
if (nums.empty()) {
return 0;
}
int size = nums.size();
// 只有一间房
if (size == 1) {
return nums[0];
}
// first: dp[i-2], second: dp[i-1]
int first = nums[0]; // dp[0]
int second = max(nums[0], nums[1]); // dp[1]
// 从第3间房(索引2)开始
for (int i = 2; i < size; i++) {
int temp = second; // 保存 dp[i-1]
second = max(first + nums[i], second); // dp[i] = max(dp[i-2]+nums[i], dp[i-1])
first = temp; // 更新 dp[i-2] = 原 dp[i-1]
}
return second; // 最终结果为 dp[n-1]
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<int> test1 = {1,2,3,1};
vector<int> test2 = {2,7,9,3,1};
vector<int> test3 = {2,1,1,2};
cout << sol.rob(test1) << "\n"; // 输出: 4
cout << sol.rob(test2) << "\n"; // 输出: 12
cout << sol.rob(test3) << "\n"; // 输出: 4
return 0;
}
JavaScript 实现
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
if (nums.length === 0) return 0;
if (nums.length === 1) return nums[0];
let first = nums[0];
let second = Math.max(nums[0], nums[1]);
for (let i = 2; i < nums.length; i++) {
let temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
};
// 测试
console.log(rob([1,2,3,1])); // 4
console.log(rob([2,7,9,3,1])); // 12
console.log(rob([2,1,1,2])); // 4
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!