经典算法:打家劫舍(动态规划 + 回溯求最优解)C++ 超详细解析

前言

打家劫舍 是动态规划的入门经典题,核心考察无相邻元素选取的最大和问题。本文将基于 C++ 实现两种核心解法:

  1. 动态规划(空间优化版):高效计算最大金额,时间 O (n),空间 O (1)
  2. 回溯法:不仅求最大金额,还能输出具体偷窃的房屋下标

代码完全可直接运行,逐行解析逻辑,新手也能轻松看懂~

一、问题描述

你是一个专业小偷,沿街有若干房屋,每间房有固定现金,相邻房屋不能同时偷窃 (否则触发警报)。给定非负整数数组表示房屋金额,求不触发警报能偷窃的最高金额

示例:输入:[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;
}
代码解析
  1. 边界处理:无房屋 / 一间房直接返回结果,避免数组越界
  2. 基础版rob1
    • dp[i]:前i间房屋能偷的最大金额
    • 遍历从 2 开始,严格套用状态转移方程
  3. 优化版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;
}
代码解析
  1. 类封装:把房屋数据、状态、方法封装,代码更优雅
  2. 回溯核心MaxRob
    • 约束条件:i==0(第一间)或前一间没偷,才能偷当前房
    • 选择:偷 / 不偷,递归遍历所有方案
    • 回溯:撤销选择,保证枚举所有可能性
  3. 结果记录:遍历完所有房屋后,更新最大金额和最优路径
  4. 输出:不仅返回最大值,还能打印哪几间房被偷

四、运行结果

动态规划版输出

复制代码
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) 可获取具体偷窃路径 需要输出最优方案下标

总结 :面试 / 刷题优先写动态规划空间优化版,需要路径时用回溯法。

六、总结

  1. 打家劫舍核心是动态规划状态转移方程dp[i] = max(dp[i-1], dp[i-2]+nums[i])
  2. 空间优化是面试高频考点,用两个变量滚动替代数组
  3. 回溯法适合需要输出具体方案的场景,掌握回溯思想

本文代码完整可直接运行,适合 C++ 新手学习动态规划和回溯法,建议收藏练习~,持续分享干货~如果对你有帮助,欢迎点赞、收藏、关注,有问题评论区交流!

相关推荐
Dev7z2 小时前
基于改进小波阈值的sEMG信号降噪与手势识别系统设计与实现
算法·手势识别·改进小波阈值·semg·信号降噪
灵感__idea9 小时前
Hello 算法:贪心的世界
前端·javascript·算法
澈20710 小时前
深入浅出C++滑动窗口算法:原理、实现与实战应用详解
数据结构·c++·算法
A.A呐10 小时前
【C++第二十九章】IO流
开发语言·c++
ambition2024211 小时前
从暴力搜索到理论最优:一道任务调度问题的完整算法演进历程
c语言·数据结构·c++·算法·贪心算法·深度优先
cmpxr_11 小时前
【C】原码和补码以及环形坐标取模算法
c语言·开发语言·算法
qiqsevenqiqiqiqi11 小时前
前缀和差分
算法·图论
代码旅人ing11 小时前
链表算法刷题指南
数据结构·算法·链表
kebeiovo11 小时前
atomic原子操作实现无锁队列
服务器·c++