【LeetCode Hot100 刷题日记 (83/100)】198. 打家劫舍 —— 数组、动态规划(线性DP)🧠

8 阅读4分钟

📌 题目链接: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),有两种选择:

  1. 偷第 i 间:那么就不能偷第 i−1 间,总金额为 dp[i−2] + nums[i]
  2. 不偷第 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) ,这是面试中非常加分的优化点!


🧩 解题思路(分步拆解)

  1. 处理边界情况

    • 如果数组为空,返回 0;
    • 如果只有一间房,直接返回 nums[0]
  2. 初始化前两个状态

    • first = nums[0]
    • second = max(nums[0], nums[1])
  3. 从第 3 间房开始遍历(i = 2 到 n−1)

    • 计算当前最大金额:max(first + nums[i], second)
    • 更新 first = secondsecond = 当前最大金额
  4. 返回最终结果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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!