打家劫舍(一)与打家劫舍(二)动态规划解法详解
- 打家劫舍(一)
- 问题描述
- 示例
- 解题思路
- 动态规划
- 代码实现
- 复杂度分析
- 打家劫舍(二)
- 问题描述
- 示例
- 解题思路
- 环形问题的拆分
- 代码实现
- 复杂度分析
- 总结
打家劫舍(一)
问题描述
你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金。不能偷相邻的两家。给定一个整数数组 nums
,数组中的元素表示每个房间存有的现金数额,计算在不被发现的前提下最多的偷窃金额。
数据范围:
数组长度满足 (1 \leq n \leq 2 \times 10^5),数组中每个值满足 (1 \leq \text{nums}[i] \leq 5000)。
示例
输入 | 输出 | 说明 |
---|---|---|
[1, 2, 3, 4] | 6 | 偷第 2、4 个房间 |
[1, 3, 6] | 7 | 偷第 1、3 个房间 |
[2, 10, 5] | 10 | 偷第 2 个房间 |
解题思路
动态规划
-
状态定义:
- 设
dp[i]
表示考虑前i
个房间时的最大偷窃金额。
- 设
-
状态转移方程:
- 当前房间有两种选择:
- 不偷:继承前一个房间的最大金额(
dp[i-1]
)。 - 偷:当前房间金额 + 前前一个房间的最大金额(
dp[i-2] + nums[i]
)。
- 不偷:继承前一个房间的最大金额(
- 因此:
[
dp[i] = \max(dp[i-1], \ dp[i-2] + \text{nums}[i])
]
- 当前房间有两种选择:
-
边界条件:
dp[0] = nums[0]
:只有一个房间时只能偷它。dp[1] = \max(nums[0], nums[1])
:两个房间时偷金额较大的。
代码实现
int rob(int* nums, int numsLen) {
// 如果数组长度小于等于0,说明没有房子可以偷,直接返回0
if (numsLen <= 0) return 0;
// 如果数组长度为1,说明只有一间房子,直接返回这间房子的金额
if (numsLen == 1) return nums[0];
// 如果数组长度为2,说明有两间房子,选择金额较大的那间房子
if (numsLen == 2) return (nums[0] > nums[1]) ? nums[0] : nums[1];
// 定义一个动态规划数组dp,dp[i]表示到第i间房子为止,小偷能偷到的最大金额
int dp[numsLen];
// 初始化dp数组的前两个值
// 如果只有一间房子,最大金额就是这间房子的金额
dp[0] = nums[0];
// 如果有两间房子,最大金额是这两间房子中金额较大的那个
dp[1] = (nums[0] > nums[1]) ? nums[0] : nums[1];
// 遍历数组,从第三间房子开始计算
for (int i = 2; i < numsLen; i++) {
// 对于第i间房子,有两种选择:
// 1. 不偷第i间房子,最大金额就是dp[i-1](即前一间房子的最大金额)
// 2. 偷第i间房子,最大金额是dp[i-2] + nums[i](即前两间房子的最大金额加上当前房子的金额)
// dp[i]的值是这两种选择中的较大值
dp[i] = (dp[i - 1] > (dp[i - 2] + nums[i])) ? dp[i - 1] : (dp[i - 2] + nums[i]);
}
// 返回dp数组的最后一个值,即所有房子的最大金额
return dp[numsLen - 1];
}
复杂度分析
- 时间复杂度:(O(n)),遍历一次数组。
- 空间复杂度:(O(n)),存储动态规划数组。
打家劫舍(二)
问题描述
沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。给定一个长度为 n
的整数数组 nums
,计算在不被发现的前提下最多的偷窃金额。
数据范围:与打家劫舍(一)相同。
示例
输入 | 输出 | 说明 |
---|---|---|
[1, 2, 3, 4] | 6 | 偷第 2、4 个房间 |
[1, 3, 6] | 6 | 偷第 3 个房间 |
解题思路
环形问题的拆分
由于首尾房间相邻,问题可拆分为两种情况:
- 不偷第一个房间:计算区间
[1, n-1]
的最大偷窃金额。 - 不偷最后一个房间:计算区间
[0, n-2]
的最大偷窃金额。
最终结果为两种情况的最大值。
代码实现
// 辅助函数:计算从 start 到 end 的最大偷窃金额
int robRange(int* nums, int start, int end) {
int n = end - start + 1; // 计算从 start 到 end 的区间长度
if (n == 1) return nums[start]; // 如果区间内只有一间房子,直接返回这间房子的金额
// 定义动态规划数组 dp,dp[i] 表示从 start 开始到第 i 间房子的最大偷窃金额
int dp[n];
dp[0] = nums[start]; // 初始化第一间房子的最大金额
dp[1] = (nums[start] > nums[start + 1]) ? nums[start] : nums[start + 1]; // 初始化前两间房子的最大金额
// 遍历从第 3 间房子到最后一间房子
for (int i = 2; i < n; i++) {
int current = start + i; // 当前房子在原数组中的索引
// 状态转移方程:选择当前房子或不选择当前房子的最大金额
dp[i] = (dp[i - 1] > (dp[i - 2] + nums[current])) ? dp[i - 1] : (dp[i - 2] + nums[current]);
}
// 返回从 start 到 end 的最大偷窃金额
return dp[n - 1];
}
int rob(int* nums, int numsLen) {
// 如果只有一间房子,直接返回这间房子的金额
if (numsLen == 1) return nums[0];
// 如果有两间房子,返回两间房子中金额较大的那个
if (numsLen == 2) return (nums[0] > nums[1]) ? nums[0] : nums[1];
// 分两种情况讨论:
// 1. 不偷最后一间房子,计算从第 0 间到第 (numsLen - 2) 间房子的最大金额
int case1 = robRange(nums, 0, numsLen - 2);
// 2. 不偷第一间房子,计算从第 1 间到第 (numsLen - 1) 间房子的最大金额
int case2 = robRange(nums, 1, numsLen - 1);
// 返回两种情况中的较大值
return (case1 > case2) ? case1 : case2;
}
复杂度分析
- 时间复杂度:(O(n)),两次遍历数组。
- 空间复杂度:(O(n)),存储动态规划数组。
总结
问题 | 动态规划状态转移方程 | 关键点 |
---|---|---|
打家劫舍(一) | dp[i] = max(dp[i-1], dp[i-2] + nums[i]) | 线性数组,直接遍历 |
打家劫舍(二) | 拆分两种情况求最大值 | 环形数组,避免首尾同时偷窃 |