打家劫舍
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路:
对于第 n 间房屋,只有两种选择:
- 不偷第 n 间 :最大金额 = 偷到第
n-1间的最大金额;- 偷第 n 间 :最大金额 = 偷到第
n-2间的最大金额 + 第n间的现金数;最终取两者的最大值,即:dp[n]=max(dp[n−1],dp[n−2]+nums[n])
解法一(记忆化递归)
代码及注释
cpp
class Solution
{
// 递归函数:计算偷到第n间房屋时的最大金额
// n:当前考虑的房屋下标(从0开始)
// nums:房屋现金数组(输入)
// res:记忆化缓存数组(存储已计算的结果,避免重复递归)
int des(int n, vector<int> &nums, vector<int> &res)
{
// 1. 记忆化核心:如果res[n]已计算过(≠-1),直接返回缓存值
// 避免重复递归,比如计算n=3时会用到n=1,若n=1已算过,直接取结果
if (res[n] != -1)
{
return res[n];
}
// 2. 状态转移:核心逻辑
// 不偷第n间:des(n-1, nums, res)
// 偷第n间:des(n-2, nums, res) + nums[n]
// 取两者最大值,存入res[n]缓存
res[n] = max(des(n - 1, nums, res), des(n - 2, nums, res) + nums[n]);
return res[n];
}
public:
int rob(vector<int> &nums)
{
int N = nums.size(); // 获取房屋总数
// 3. 初始化记忆化数组:初始值-1(标记未计算)
// 为什么用-1?因为房屋现金是「非负整数」,0是合法值,不能用0标记未计算
vector<int> res(N, -1);
// 4. 边界条件1:没有房屋,返回0
if (N == 0)
{
return 0;
}
// 5. 边界条件2:只有1间房屋,直接偷这一间
res[0] = nums[0];
if (N == 1)
{
return res[0];
}
// 6. 边界条件3:有2间房屋,偷金额更大的那一间
res[1] = max(nums[0], nums[1]);
// 7. 递归计算:偷到最后一间(下标N-1)的最大金额
return des(N - 1, nums, res);
}
};
可优化点:
- 递归栈风险:如果房屋数量极多(比如 10^5),递归会导致栈溢出(可以改用「迭代版动态规划」);
- 空间优化:迭代版可以只用两个变量代替缓存数组,空间复杂度降为 O(1)。
解法二(基础递推)
同样基于打家劫舍的核心状态转移方程:dp[i]=max(dp[i−1],dp[i−2]+nums[i])
- 递归版(自顶向下) :从最后一间房屋(
N-1)出发,递归拆解为计算i-1、i-2,直到触达边界(i=0/i=1); - 递推版(自底向上) :从边界(
i=0/i=1)出发,逐步计算到最后一间房屋(i=N-1),完全用循环替代递归,更直观。
代码及注释
cpp
class Solution
{
public:
int rob(vector<int> &nums)
{
int N = nums.size(); // 获取房屋总数
// 边界条件1:没有房屋,直接返回0
if (N == 0)
{
return 0;
}
// 边界条件2:只有1间房屋,偷这一间即可
if (N == 1)
{
return nums[0];
}
// 初始化dp数组(这里命名为res,本质是动态规划的状态数组)
// res[i] 表示「偷到第i间房屋时的最大金额」
// 初始值-1仅为占位,后续会被覆盖(和递归版不同,这里-1无"未计算"的标记意义)
vector<int> res(N, -1);
// 边界条件3:第0间房屋的最大金额 = 自身金额
res[0] = nums[0];
// 边界条件4:第1间房屋的最大金额 = 选0/1间中金额更大的
res[1] = max(nums[0], nums[1]);
// 核心:从第2间开始,逐行递推计算每间的最大金额
for (int i = 2; i < N; i++)
{
// 状态转移:
// 不偷第i间 → 最大金额 = res[i-1](偷到i-1间的最大值)
// 偷第i间 → 最大金额 = res[i-2] + nums[i](偷到i-2间的最大值 + 第i间金额)
// 取两者最大值作为res[i]
res[i] = max(res[i - 1], res[i - 2] + nums[i]);
}
// 最终结果:偷到最后一间(N-1)的最大金额
return res[N - 1];
}
};
解法三(空间优化递推)
cpp
class Solution
{
public:
int rob(vector<int> &nums)
{
int N = nums.size(); // 获取房屋总数
// 边界条件1:没有房屋,返回0
if (N == 0)
{
return 0;
}
// 边界条件2:只有1间房屋,直接偷这一间
if (N == 1)
{
return nums[0];
}
// 初始化前两个状态(替代dp数组的前两个值)
int f1 = nums[0]; // 对应dp[0]:偷第0间的最大金额
int f2 = max(nums[0], nums[1]); // 对应dp[1]:偷到第1间的最大金额
// 核心:从第2间开始,滚动计算每一间的最大金额
for (int i = 2; i < N; i++)
{
// 计算当前间的最大金额:max(不偷当前间, 偷当前间)
// 不偷当前间 → 最大金额 = f2(偷到i-1间的最大值)
// 偷当前间 → 最大金额 = f1 + nums[i](偷到i-2间的最大值 + 当前间金额)
int f3 = max(f2, f1 + nums[i]);
// 滚动更新:为下一次循环做准备
f1 = f2; // f1 变为新的 dp[i-2](原dp[i-1])
f2 = f3; // f2 变为新的 dp[i-1](原dp[i])
}
// 最终f2就是dp[N-1](偷到最后一间的最大金额)
return f2;
}
};
- 核心优化思路:状态只依赖前两个值 → 用变量替代数组,这个思路可迁移到所有 "线性递推、仅依赖前 k 个状态" 的动态规划问题(比如爬楼梯、斐波那契数列);
- 执行逻辑:从边界出发,通过循环滚动更新状态,最终得到最后一个状态的结果;
f3 = max(f2, f1 + nums[i])+ 滚动更新f1=f2、f2=f3。