打劫家舍Ⅰ
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
代码1(自己的)
class Solution {
public:
int rob(vector<int>& nums) {
// 如果没有房屋,则无法抢劫,返回 0
if (nums.empty()) return 0;
// dp数组用于存储可以抢到的最高金额
vector<int> dp(nums.size(), 0);
// 初始化dp数组的第一个元素,即如果只有一幢房子,则只能抢这一幢
dp[0] = nums[0];
int sum = 0;
// 动态规划,从第二幢房子开始考虑(如果有的话)
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < i - 1; j++) {
// 对每一幢房子i,检查所有可能的不过关系式
// 当前房子选择最大历史收益
dp[i] = max(dp[j] + nums[i], dp[i]);
}
}
// 计算在所有房子的情况下,可以获得的最大金额
for (auto a : dp) {
sum = max(sum, a);
}
return sum;
}
};
代码:(官方的)
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) return 0;
if (nums.size() == 1) return nums[0];
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); ++i) {
dp[i] = max(dp[i - 1], nums[i] + dp[i - 2]);
}
return dp[nums.size() - 1];
}
};
打劫家舍Ⅱ
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3] 输出:3
代码:
class Solution {
public:
int rob(vector<int>& nums) {
// 如果没有房屋,返回0
if (nums.size() == 0) return 0;
// 如果只有一间房屋,返回该房屋的金额
if (nums.size() == 1) return nums[0];
// 考虑不抢最后一个房屋的情况
int result1 = robRange(nums, 0, nums.size() - 2);
// 考虑不抢第一个房屋的情况
int result2 = robRange(nums, 1, nums.size() - 1);
// 返回两种情况中获得的最大金额
return max(result1, result2);
}
// 处理范围内的子序列,不算第一次房子和最后一次房子同时包含的情况
int robRange(vector<int>& nums, int start, int end) {
// 如果开始和结束是同一个房子,直接返回该房子的金额
if (end == start) return nums[start];
// dp数组被用来记录到每个房子的最高抢劫金额
vector<int> dp(nums.size());
// 初始化
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);
// 计算从start到end范围内的最大可抢金额
for (int i = start + 2; i <= end; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
// 返回在end位置能最大抢劫的金额
return dp[end];
}
};
-
将问题划分为两种情况:
- 不抢最后一个房子:在
nums
范围内从0
到n-2
(通过调用robRange
实现) - 不抢第一个房子:在
nums
范围内从1
到n-1
(通过调用robRange
实现)
- 不抢最后一个房子:在
-
robRange 函数:
- 功能:处理没有环形限制情况下,房屋排列的最大可抢金额。
- 初始化:
dp[start]
是开始房子的金额,dp[start + 1]
是前两个房子的最大金额。 - 动态规划:从
start + 2
到end
计算每一个位置能获取的最大金额。
-
时间复杂度和空间复杂度分析:
- 由于分离了两个固定子问题(
0
到n-2
和1
到n-1
),时间复杂度为 O(n)。 - 动态规划数组的使用导致空间复杂度为 O(n),可以进一步优化为 O(1) 使用滚动变量。
- 由于分离了两个固定子问题(
打劫家舍Ⅲ
题目描述:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1] 输出: 7 解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1] 输出: 9 解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
代码:
class Solution {
public:
int rob(TreeNode* root) {
// robTree 函数返回的结果向量
// result[0] 表示不偷当前节点的最大收益
// result[1] 表示偷当前节点的最大收益
vector<int> result = robTree(root);
// 返回根节点偷或不偷的最大值
return max(result[0], result[1]);
}
vector<int> robTree(TreeNode* cur) {
// 如果当前节点为空,返回{0, 0}
if (cur == NULL) return vector<int>{0, 0};
// 分别计算左右子树的偷与不偷的最大收益
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
// 偷当前节点,不能偷子节点
int val1 = cur->val + left[0] + right[0];
// 不偷当前节点,可以选择偷或不偷子节点,取决于利润更大
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
// 返回不偷和偷当前节点时的可得最大收益
return {val2, val1};
}
};
1. 确定两个状态
对于每一个节点,我们有两个状态:
- 不偷当前节点的最大收益 (
val2
):可以选择偷或不偷子节点。 - 偷当前节点的最大收益 (
val1
):不能偷子节点。
2. 递推公式
- 偷当前节点(
val1
):val1 = cur->val + left[0] + right[0]
cur->val
是偷当前节点获得的收益。left[0]
是不偷左子节点时能得的最大收益。right[0]
是不偷右子节点时能得的最大收益。
- 不偷当前节点(
val2
):val2 = max(left[0], left[1]) + max(right[0], right[1])
max(left[0], left[1])
取决于左子节点偷不偷的更大利益。max(right[0], right[1])
取决于右子节点偷不偷的更大利益。
3. 递归终止条件
如果当前节点是空节点(cur == NULL
),则直接返回{0, 0}
,因为空节点下的偷与不偷收益均为0。
4. 计算最大收益
调用robTree
函数后,得到根节点不偷和偷的最大收益分别为result[0]
和result[1]
,取最大值即为最终最大收益。
5. 时间复杂度和空间复杂度
- 时间复杂度:O(n),树的每个节点都会计算一次,不会重复计算。
- 空间复杂度:O(h),h为树的高度,使用递归栈空间。