前言
打家劫舍 是动态规划的入门经典题,核心考察无相邻元素选取的最大和问题。本文将基于 C++ 实现两种核心解法:
- 动态规划(空间优化版):高效计算最大金额,时间 O (n),空间 O (1)
- 回溯法:不仅求最大金额,还能输出具体偷窃的房屋下标
代码完全可直接运行,逐行解析逻辑,新手也能轻松看懂~
一、问题描述
你是一个专业小偷,沿街有若干房屋,每间房有固定现金,相邻房屋不能同时偷窃 (否则触发警报)。给定非负整数数组表示房屋金额,求不触发警报能偷窃的最高金额。
示例:输入:[12,1,3,23]输出:35(偷窃 12+23,总和最大)
二、核心思路
1. 动态规划核心公式
这是解题的关键!对于第i间房屋,只有两种选择:
- 不偷:最大金额 = 前
i-1间的最大值 - 偷:最大金额 = 前
i-2间的最大值 + 当前房屋金额
最终状态转移方程:
cpp
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
2. 算法优化
- 基础版:用数组
dp存储所有状态(空间 O (n)) - 优化版:只用两个变量保存前两个状态(空间 O (1),最优解)
三、代码实现与逐行解析
版本 1:动态规划实现(推荐)
包含基础数组版 和空间优化版,工业级最优写法:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 打家劫舍:动态规划 基础版(使用vector存储dp数组)
int rob1(const std::vector<int>& ar)
{
int len = ar.size();
if (len == 0) return 0; // 无房屋,收益0
if (len == 1) return ar[0];// 只有一间房,直接偷
vector<int> dp(len + 1, 0);// dp[i]表示前i间房的最大收益
dp[0] = 0;
dp[1] = ar[0]; // 第一间房的最大收益
for (int i = 2; i <= len; ++i)
{
// 核心公式:不偷i / 偷i+前i-2间的最大值
dp[i] = std::max(dp[i - 1], dp[i - 2] + ar[i - 1]);
}
return dp[len];
}
// 动态规划 优化版(不使用vector,空间O(1))
int rob2(const std::vector<int>& ar)
{
int len = ar.size();
if (len == 0) return 0;
if (len == 1) return ar[0];
// pre = dp[i-2], cur = dp[i-1],用变量替代数组
int pre = 0;
int cur = ar[0];
for (int i = 2; i <= len; ++i)
{
// 计算当前最大值
int tmp = std::max(cur, pre + ar[i - 1]);
// 状态滚动更新
pre = cur;
cur = tmp;
}
return cur;
}
int main()
{
std::vector<int> ar1 = { 1,2,3,1 }; // 预期4
std::vector<int> ar2 = { 2,7,9,3,1 }; // 预期12
std::vector<int> ar3 = { 12,2,3,23 }; // 预期35
// 调用优化版函数
cout << rob2(ar1) << "( 4 )" << endl;
cout << rob2(ar2) << "( 12 )" << endl;
cout << rob2(ar3) << "( 35 )" << endl;
return 0;
}
代码解析
- 边界处理:无房屋 / 一间房直接返回结果,避免数组越界
- 基础版
rob1:dp[i]:前i间房屋能偷的最大金额- 遍历从 2 开始,严格套用状态转移方程
- 优化版
rob2(重点):- 不需要保存所有 dp 值,只需要前两个状态
pre= dp[i-2],cur= dp[i-1]- 每次计算后滚动更新变量,空间复杂度从 O (n)→O (1)
版本 2:回溯法(求最大金额 + 偷窃路径)
如果需要知道具体偷了哪几间房,用回溯法枚举所有合法方案,记录最优解:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Rob
{
private:
std::vector<int> ar; // 房屋金额数组
std::vector<int> cur; // 临时标记:1=偷,0=不偷
std::vector<int> vcur; // 最终最优路径
int len; // 房屋数量
int maxsum; // 最大金额
int cursum; // 当前方案总金额
// 打印数组工具函数
static void PrintVec(const std::vector<int>& a)
{
int n = a.size();
for (int i = 0; i < n; ++i)
{
printf("%5d", a[i]);
}
printf("\n----------------------\n");
}
// 回溯核心函数:枚举所有偷窃方案
void MaxRob(int i, int n)
{
// 递归终止:遍历完所有房屋
if (i >= n)
{
// 更新最大值和最优路径
if (cursum > maxsum)
{
maxsum = cursum;
vcur = cur;
}
}
else
{
// 约束:前一间没偷,才能偷当前间
if (i == 0 || cur[i - 1] == 0)
{
cur[i] = 1; // 标记偷
cursum += ar[i]; // 累加金额
MaxRob(i + 1, n); // 递归下一间
cursum -= ar[i]; // 回溯:撤销选择
cur[i] = 0; // 取消标记
}
MaxRob(i + 1, n); // 不偷当前间,直接下一间
}
}
public:
// 构造函数:初始化变量
Rob(const std::vector<int>& nums)
{
len = nums.size();
ar = nums;
cur.resize(len, 0);
maxsum = 0;
cursum = 0;
}
// 获取最大金额
int maxSum()
{
if (len == 0) return 0;
if (len == 1) return ar[0];
MaxRob(0, len);
return maxsum;
}
// 打印最优偷窃路径(1=偷,0=不偷)
void Print() const
{
for (auto& x : vcur)
{
printf("%5d", x);
}
printf("\n---------------\n");
}
};
int main()
{
std::vector<int> ar3 = { 12,2,3,23 };
Rob rob(ar3);
cout << "最大偷窃金额:" << rob.maxSum() << endl;
cout << "偷窃路径(1=偷,0=不偷):";
rob.Print();
return 0;
}
代码解析
- 类封装:把房屋数据、状态、方法封装,代码更优雅
- 回溯核心
MaxRob:- 约束条件:
i==0(第一间)或前一间没偷,才能偷当前房 - 选择:偷 / 不偷,递归遍历所有方案
- 回溯:撤销选择,保证枚举所有可能性
- 约束条件:
- 结果记录:遍历完所有房屋后,更新最大金额和最优路径
- 输出:不仅返回最大值,还能打印哪几间房被偷
四、运行结果
动态规划版输出
4( 4 )
12( 12 )
35( 35 )
回溯法版输出
最大偷窃金额:35
偷窃路径(1=偷,0=不偷): 1 0 0 1
✅ 完美匹配预期结果:偷第 1 间和第 4 间,12+23=35
五、两种方案对比
| 方案 | 时间复杂度 | 空间复杂度 | 优点 | 适用场景 |
|---|---|---|---|---|
| 动态规划 (优化) | O(n) | O(1) | 效率极高,工业级最优解 | 只需要求最大金额 |
| 回溯法 | O(2ⁿ) | O(n) | 可获取具体偷窃路径 | 需要输出最优方案下标 |
总结 :面试 / 刷题优先写动态规划空间优化版,需要路径时用回溯法。
六、总结
- 打家劫舍核心是动态规划状态转移方程 :
dp[i] = max(dp[i-1], dp[i-2]+nums[i]) - 空间优化是面试高频考点,用两个变量滚动替代数组
- 回溯法适合需要输出具体方案的场景,掌握回溯思想
本文代码完整可直接运行,适合 C++ 新手学习动态规划和回溯法,建议收藏练习~,持续分享干货~如果对你有帮助,欢迎点赞、收藏、关注,有问题评论区交流!