📖 问题描述
你是一个职业小偷,计划偷窃沿街的房屋。每间房内有一定现金,但相邻房屋装有防盗系统,如果两间相邻的房屋在同一晚被闯入,系统会自动报警。给定一个非负整数数组表示每个房屋的金额,计算在不触发警报的情况下,一夜能偷到的最高金额。
🌰 示例分析
示例1:
输入:[1,2,3,1]
输出:4
解释:偷第1号房(金额=1)和第3号房(金额=3),总计4。
示例2:
输入:[2,7,9,3,1]
输出:12
解释:偷第1号(2)、第3号(9)和第5号(1),总计12。
🧠 解题思路(动态规划四步法)
1. 定义状态
假设我们偷到第 i
个房屋时,能获得的最大金额为 dp[i]
。此时有两种选择:
-
不偷第i间:最大金额等于偷到前
i-1
间的最大值,即dp[i-1]
。 -
偷第i间:因为不能偷相邻的,最大金额等于偷到前
i-2
间的最大值 + 第i间的金额,即dp[i-2] + nums[i]
。
取这两种选择的较大值,得到状态转移方程:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
2. 初始化
-
只有1间房时,必须偷:
dp[0] = nums[0]
-
有2间房时,偷金额较大的那间:
dp[1] = max(nums[0], nums[1])
3. 填表顺序
从第3间房开始,逐步计算到最后一间。
4. 空间优化
观察状态转移方程,发现 dp[i]
只依赖前两个状态。因此只需用两个变量滚动更新,无需存储整个数组,空间复杂度从 O(n)
降为 O(1)
。
🖥️ 代码实现
方法一:动态规划(空间优化版)
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
if (n == 1) return nums[0];
int prev = nums[0]; // 代表 dp[i-2]
int curr = Math.max(nums[0], nums[1]); // 代表 dp[i-1]
for (int i = 2; i < n; i++) {
int temp = curr;
curr = Math.max(curr, prev + nums[i]);
prev = temp;
}
return curr;
}
}
方法二:记忆化搜索(递归+备忘录)
class Solution {
private int[] nums, memo;
public int rob(int[] nums) {
this.nums = nums;
int n = nums.length;
memo = new int[n];
Arrays.fill(memo, -1); // 初始化备忘录
return dfs(n - 1); // 从最后一间房开始决策
}
// 定义:偷到第i间房时的最大金额
private int dfs(int i) {
if (i < 0) return 0; // 没有房屋可偷
if (memo[i] != -1) return memo[i]; // 查备忘录
// 选择偷或不偷第i间房,取较大值
int res = Math.max(dfs(i - 1), dfs(i - 2) + nums[i]);
memo[i] = res; // 存备忘录
return res;
}
}
📊 复杂度分析
-
时间复杂度:
O(n)
,每个房屋只需计算一次。 -
空间复杂度:
-
动态规划(优化后):
O(1)
-
记忆化搜索:
O(n)
(递归栈深度 + 备忘录)
-
💡 总结与思考
-
动态规划核心:通过子问题的最优解推导全局最优,避免重复计算。
-
适用场景:问题具有重叠子问题和最优子结构时。
-
举一反三:若房屋成环形(首尾相连),如何解决?不妨拆分为两个线性问题:不偷第一间或不偷最后一间,取较大值。
练习推荐:LeetCode 213(打家劫舍II)、337(二叉树形式打家劫舍)。