力扣解题-198. 打家劫舍

5 阅读8分钟

力扣解题-198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]

输出:4

解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]

输出:12

解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

1 <= nums.length <= 100

0 <= nums[i] <= 400

Related Topics

数组、动态规划


第一次解答

解题思路

核心方法:动态规划空间优化版(滚动变量法),基于打家劫舍问题的核心递推规律,通过两个滚动变量记录前两步的最大金额,避免创建完整的dp数组,将空间复杂度从O(n)降至O(1),时间复杂度保持O(n),是本题的高效最优解法。

核心逻辑拆解

打家劫舍问题的核心是“选或不选当前房屋”的最优决策:

  1. 递推公式推导
    • 对于第i间房屋,有两种选择:
      • 不偷第i间:最大金额等于偷到第i-1间的最大金额(f(i-1));
      • 偷第i间:不能偷第i-1间,最大金额等于偷到第i-2间的最大金额 + 第i间的金额(f(i-2) + nums[i]);
    • 因此状态转移公式:f(i) = max(f(i-1), f(i-2) + nums[i])
  2. 初始条件
    • f(0) = nums[0]:只有1间房屋时,偷这一间的金额就是最大值;
    • f(1) = max(nums[0], nums[1]):有2间房屋时,选金额更大的那一间;
  3. 滚动变量优化:无需存储所有f(0)f(n-1)的值,仅用两个变量记录f(i-2)f(i-1),迭代计算f(i)
具体执行逻辑
  1. 边界处理
    • 若数组为空(nums==null || nums.length==0),返回0;
    • 若数组长度为1,直接返回nums[0](只有一间房,偷它);
  2. 初始化滚动变量
    • prev0 = nums[0]:代表f(i-2)(初始为f(0),即偷第0间的金额);
    • prev1 = Math.max(nums[0], nums[1]):代表f(i-1)(初始为f(1),即前两间房的最大金额);
  3. 迭代计算(从i=2i=nums.length-1):
    • current = Math.max(prev1, prev0 + nums[i]):计算第i间房的最优解(不偷当前房取prev1,偷当前房取prev0+nums[i]);
    • prev0 = prev1:更新f(i-2)为原f(i-1)
    • prev1 = current:更新f(i-1)为当前计算的f(i)
  4. 返回结果:迭代结束后,prev1即为偷到最后一间房的最大金额。
执行流程可视化(以示例2 nums=[2,7,9,3,1]为例)
迭代iprev0(f(i-2))prev1(f(i-1))nums[i]current(f(i))变量更新后
22(f(0))7(f(1))9max(7,2+9)=11prev0=7、prev1=11
37(f(1))11(f(2))3max(11,7+3)=11prev0=11、prev1=11
411(f(2))11(f(3))1max(11,11+1)=12prev0=11、prev1=12
结束----返回12
关键细节说明
  • 初始条件的合理性f(1)取前两间房的最大值,符合“不能偷相邻房屋”的规则;
  • 变量更新顺序:先计算current,再更新prev0prev1,避免覆盖未使用的旧值;
  • 非负金额适配:题目规定nums[i]≥0,无需处理负数金额的特殊情况;
  • 循环范围:从i=2开始,覆盖所有≥3间房屋的场景。
性能说明
  • 时间复杂度:O(n)(仅需一次线性迭代,每个房屋仅计算一次);
  • 空间复杂度:O(1)(仅使用3个变量,无额外数组);
  • 优势:
    1. 空间效率最优,避免了dp数组的内存开销;
    2. 代码简洁,迭代逻辑清晰,无递归栈开销;
    3. 时间效率高,线性迭代无冗余计算,适配题目n≤100的规模。
public int rob(int[] nums) {
    if(nums==null || nums.length==0){
        return 0;
    }
    if(nums.length==1){
        return nums[0];
    }
    int prev0=nums[0];//f(0)
    int prev1=Math.max(nums[0],nums[1]);//f(1)
    //重要的公式 f(i)=max(f(i-1),f(i-2)+nums[i])
    for(int i=2;i<nums.length;i++){
        int current=Math.max(prev1,prev0+nums[i]);//f(i)=max(f(i-1),f(i-2)+nums[i])
        prev0=prev1;
        prev1=current;
    }
    return prev1;
}

示例解答

解题思路

解法1:标准动态规划法(直观版,O(n)时间+O(n)空间)

核心方法:创建dp数组存储每一步的最大金额,直接按递推公式填充数组,逻辑更直观,适合理解动态规划的核心思想(状态定义、初始条件、状态转移)。

代码实现
public int rob(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int n = nums.length;
    if (n == 1) {
        return nums[0];
    }
    // dp[i] 表示偷到第i间房屋时的最大金额
    int[] dp = new int[n];
    // 初始条件
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);
    // 状态转移
    for (int i = 2; i < n; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
    }
    // 最后一个元素即为偷到最后一间房的最大金额
    return dp[n-1];
}
核心逻辑说明
  1. dp数组定义dp[i]表示偷到第i间房屋(索引从0开始)时,不触发警报的最大金额;
  2. 初始条件dp[0]=nums[0]dp[1]=max(nums[0],nums[1]),与滚动变量法一致;
  3. 状态转移:从i=2开始,按dp[i] = max(dp[i-1], dp[i-2]+nums[i])填充数组;
  4. 结果返回dp[n-1]即为偷完所有房屋的最大金额。
性能说明
  • 时间复杂度:O(n)(与滚动变量法一致);
  • 空间复杂度:O(n)(需要存储n个元素的dp数组);
  • 优势:
    1. 逻辑直观,完全贴合动态规划“状态定义-初始条件-状态转移”的经典范式;
    2. 便于调试,可查看每一步的dp值,理解最优决策的形成过程;
  • 劣势:空间复杂度高于滚动变量法,n较大时(如n=1e5)内存开销更大。
解法2:递归+记忆化(Top-Down,O(n)时间+O(n)空间)

核心方法:通过递归实现“选或不选当前房屋”的决策,并用记忆化缓存避免重复计算,将纯递归的O(2ⁿ)时间复杂度优化至O(n),适合理解自顶向下的解题思路。

代码实现
public int rob(int[] nums) {
    // 记忆化缓存,存储偷到第i间房屋的最大金额
    int[] memo = new int[nums.length];
    // 初始化缓存为-1,表示未计算
    Arrays.fill(memo, -1);
    return dfs(nums, nums.length - 1, memo);
}

private int dfs(int[] nums, int i, int[] memo) {
    // 边界条件:没有房屋可偷
    if (i < 0) {
        return 0;
    }
    // 已计算过,直接返回缓存值
    if (memo[i] != -1) {
        return memo[i];
    }
    // 递归决策:不偷第i间(dfs(i-1)) 或 偷第i间(dfs(i-2)+nums[i])
    int res = Math.max(dfs(nums, i-1, memo), dfs(nums, i-2, memo) + nums[i]);
    // 缓存结果
    memo[i] = res;
    return res;
}
核心逻辑说明
  1. 记忆化缓存memo数组存储已计算的dfs(i),避免重复递归计算(如计算dfs(4)时无需重复计算dfs(2));
  2. 递归终止条件i < 0时返回0(没有房屋可偷,金额为0);
  3. 递归决策dfs(i) = max(dfs(i-1), dfs(i-2)+nums[i]),对应“不偷当前房”和“偷当前房”两种选择;
  4. 结果返回:递归到i = nums.length-1时,返回的结果即为偷完所有房屋的最大金额。
性能说明
  • 时间复杂度:O(n)(每个dfs(i)仅计算一次);
  • 空间复杂度:O(n)(递归栈深度+memo数组);
  • 优势:
    1. 符合“自顶向下”的解题思路,直观体现“选或不选”的决策逻辑;
    2. 避免了纯递归的指数级时间复杂度,效率与动态规划持平;
  • 劣势:
    1. 递归栈有额外开销,n=100时栈深度为100,无溢出风险但效率略低;
    2. 空间复杂度高于滚动变量法。

总结

  1. 滚动变量动态规划(第一次解答):O(n)时间+O(1)空间,空间最优,工程实践首选;
  2. 标准动态规划法:O(n)时间+O(n)空间,逻辑直观,适合理解动态规划核心思想;
  3. 递归+记忆化:O(n)时间+O(n)空间,自顶向下解题,易于理解决策逻辑;
  4. 关键技巧
    • 核心思想:打家劫舍的核心是“选或不选当前房屋”的最优决策,状态转移公式为f(i) = max(f(i-1), f(i-2)+nums[i])
    • 优化方向:从标准DP(O(n)空间)→ 滚动变量(O(1)空间)是工程最优选择;
    • 场景选择:日常开发选滚动变量法,教学选标准DP/记忆化递归,便于理解核心逻辑。