经典算法:打家劫舍(动态规划 + 回溯求最优解)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++ 新手学习动态规划和回溯法,建议收藏练习~,持续分享干货~如果对你有帮助,欢迎点赞、收藏、关注,有问题评论区交流!

相关推荐
qeen871 分钟前
【数据结构】建堆的时间复杂度讨论与TOP-K问题
c语言·数据结构·c++·学习·
图码12 分钟前
如何用多种方法判断字符串是否为回文?
开发语言·数据结构·c++·算法·阿里云·线性回归·数字雕刻
handler0120 分钟前
Linux 内核剖析:进程优先级、上下文切换与 O(1) 调度算法
linux·运维·c语言·开发语言·c++·笔记·算法
zhouwy11325 分钟前
Linux进程与线程编程详解
linux·c++
minglie127 分钟前
实数列的常用递推模式
算法
代码小书生1 小时前
math,一个基础的 Python 库!
人工智能·python·算法
AI科技星1 小时前
全域数学·数术本源·高维代数卷(72分册)【乖乖数学】
人工智能·算法·数学建模·数据挖掘·量子计算
生成论实验室1 小时前
《事件关系阴阳博弈动力学:识势应势之道》第一篇:生成正在发生——从《即事经》到事件-关系网络
人工智能·科技·算法·架构·创业创新
漂流瓶jz1 小时前
UVA-1152 和为0的4个值 题解答案代码 算法竞赛入门经典第二版
数据结构·算法·二分查找·题解·aoapc·算法竞赛入门经典·uva
leoufung1 小时前
LeetCode 76:Minimum Window Substring 题解与滑动窗口思维详解
算法·leetcode·职场和发展