打家劫舍
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
解题思路
-
确定dp数组以及下标的含义
dp[i]
表示房屋总数数量为i的房间里能够偷窃到最大金额为dp[i]
-
确定递推公式
- 针对第i间房子,只有两种偷窃方式,一种是偷,另一种是不偷,如果是偷的话,由于相邻两间是不能同时偷的,因此偷窃的最大金额依赖于dp[i-2],如果不偷当前房子,那么偷窃的最大金额为dp[i-1],然后取二者中的较大值即可得到最多的偷窃金额。
- 递推公式:
dp[i] = max(dp[i-2] + nums[i],dp[[i-1])
-
dp数组初始化
- 当只有一间屋子的时候,dp[0]自然初始化为nums[0],如果有两件屋子的情况下,dp[1]初始化为nums[0]和nums[1]中的较大值,这样才能保证偷窃的金额为最大值。
-
确定遍历顺序
- 根据递推公式可知,dp[i]数组依赖dp[i-1]和dp[i-2],因此需要初始化dp[0]和dp[1],遍历顺序是正序遍历
-
举例推导dp数组
-
以示例二,输入[2,7,9,3,1]为例,手动推导dp数组状态图:
-
代码实现
测试地址:https://leetcode.cn/problems/house-robber/
class Solution {
public:
int rob(vector<int> &nums) {
// 处理空数组的情况
if (nums.size() == 0)
return 0;
// 如果数组只有一个元素,则直接返回该元素
if (nums.size() == 1)
return nums[0];
// 初始化dp数组,用于存储到每个位置时能抢劫到的最大金额
vector<int> dp(nums.size() + 1, 0);
dp[0] = nums[0]; // 只有一个房子时,直接抢劫该房子
dp[1] = max(nums[0], nums[1]); // 比较前两个房子,选择金额较大的房子抢劫
// 从第三个房子开始,遍历数组
for (int i = 2; i < nums.size(); i++) {
// 对于当前房子nums[i],有两种选择:
// 1. 抢劫当前房子,加上i-2位置的最大金额(因为不能连续抢劫)
// 2. 不抢劫当前房子,保持i-1位置的最大金额
// 选择上述两种方案中的较大值
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
// 返回最后一个房子位置的最大金额,即为抢劫到的最大金额
return dp[nums.size() - 1];
}
};
打家劫舍 II
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 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
解题思路
-
确定dp数组以及下标的含义
dp[i]
表示当考虑到第i
个房屋时,能够获得的最大收益。下标i
对应于数组nums
中的第i
个元素,意味着我们考虑偷窃从第0个到第i个房屋的最优解。
-
确定递推公式
-
考虑第
i
个房屋时,我们有两种选择:- 不偷第
i
个房屋,此时的总收益等于偷窃到第i-1
个房屋的最大收益,即dp[i-1]
。 - 偷第
i
个房屋,此时的总收益等于第i
个房屋的价值加上偷窃到第i-2
个房屋的最大收益,即nums[i] + dp[i-2]
。
- 不偷第
-
因此,递推公式为:
dp[i]=max(dp[i−2]+nums[i],dp[i−1])
-
-
dp数组初始化
dp[0]
应初始化为nums[0]
,因为当我们只有一个房屋时,最大收益就是这个房屋的价值。- 当有至少两个房屋时,
dp[1]
应初始化为max(nums[0], nums[1])
,这是因为我们只能选择这两个房屋中价值较大的一个(无法同时选择两个相邻的房屋)。
-
确定遍历顺序
- 由于
dp[i]
的值依赖于dp[i-1]
和dp[i-2]
,我们必须从左到右进行遍历,这样能确保在计算dp[i]
的值时,dp[i-1]
和dp[i-2]
都已经被正确计算。
- 由于
-
举例推导dp数组
-
以下是
dp
数组的推导过程示例,考虑nums = [2, 7, 9, 3, 1]
:-
偷第一个房子,不偷最后一个房子
- 初始化
i = 0,i = 1
:dp[0] = 2
,dp[1] = 7
i = 2
:dp[2] = max(dp[0] + nums[2], dp[1]) = max(2 + 9, 7) = 11
i = 3
:dp[3] = max(dp[1] + nums[3], dp[2]) = max(7 + 3, 11) = 11
- 初始化
-
不偷第一个房子,偷最后一个房子
- 初始化
i = 0,i = 1
:dp[0] = 7
,dp[1] = 9
i = 2
:dp[2] = max(dp[0] + nums[2], dp[1]) = max(7 + 3, 9) = 10
i = 3
:dp[3] = max(dp[1] + nums[3], dp[2]) = max(9 + 1, 10) = 10
- 初始化
最终,
dp[3]
为 12,表明考虑所有房屋时,可以获得的最大收益为 11。 -
-
代码实现
测试地址:https://leetcode.cn/problems/house-robber-ii/
class Solution {
public:
// solve函数用于求解给定列表的最大收益
int solve(vector<int> &nums) {
// 特殊情况处理:如果数组为空,返回0
if (nums.size() == 0)
return 0;
// 特殊情况处理:如果数组只有一个元素,返回该元素的值
if (nums.size() == 1)
return nums[0];
// 初始化动态规划数组
vector<int> dp(nums.size() + 1, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
// 动态规划求解最大收益
for (int i = 2; i < nums.size(); i++) {
// 状态转移方程:dp[i]为偷窃到第i个房屋时可得到的最大收益
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
// 返回偷窃全部房屋可得到的最大收益
return dp[nums.size() - 1];
}
// rob函数主要处理原问题可能存在的环形结构
int rob(vector<int> &nums) {
// 特殊情况处理:如果数组只有一个元素,返回该元素的值
if (nums.size() == 1)
return nums[0];
// 分别去掉原数组的第一个元素和最后一个元素,得到两个子数组
vector<int> subNums1(nums.begin(), nums.end() - 1);
vector<int> subNums2(nums.begin() + 1, nums.end());
// 返回两种情况下的最大值,作为最后的结果
return max(solve(subNums1), solve(subNums2));
}
};
优化空间后的写法如下:
class Solution {
public:
// 主函数,解决打家劫舍问题
int rob(vector<int>& nums) {
// 特殊情况处理
if (nums.size() == 0) return 0; // 如果数组为空,则返回0
if (nums.size() == 1) return nums[0]; // 如果数组只有一个元素,则返回该元素
if (nums.size() == 2) return max(nums[0], nums[1]); // 如果数组有两个元素,则返回其中较大的一个
// 创建两个子数组:一个不包含第一个元素,另一个不包含最后一个元素
vector<int> nums1(nums.begin(), nums.end()-1);
vector<int> nums2(nums.begin()+1, nums.end());
// 返回两种情况下可能的最大值
return max(robRange(nums1), robRange(nums2));
}
private:
// 辅助函数,用于计算标准(非环形)房屋范围内可抢劫的最大金额
int robRange(vector<int>& nums) {
int dp_i_1 = 0, dp_i_2 = 0; // 分别初始化dp[i-1]和dp[i-2]
int dp_i = 0; // 初始化当前位置的最大值
for (int i = 0; i < nums.size(); i++) {
// 计算并更新dp[i]
dp_i = max(dp_i_1, dp_i_2 + nums[i]);
// 更新dp[i-2]和dp[i-1]到下一个状态
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
// 返回最终的最大值
return dp_i;
}
};
打家劫舍 III
题目描述
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 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
解题思路
-
确定dp数组以及下标的含义
-
“dp数组” 实际上是一个大小为2的数组,存储与每个节点相关的两个状态:
dp[0]
表示如果不偷这个节点,可以从这个节点的子树获得的最大收益。dp[1]
表示如果偷这个节点,可以从这个节点的子树获得的最大收益。
-
-
确定递推公式
- 对于
dp[0]
(不偷当前节点)的情况,收益是左子节点的最大收益(无论偷或不偷)加上右子节点的最大收益(无论偷或不偷),因此递推公式为:max(leftdp[0], leftdp[1]) + max(rightdp[0], rightdp[1]);
- 对于
dp[1]
(偷当前节点)的情况,收益是当前节点的值加上左右子节点不偷时的收益,因此递推公式为:cur->val + leftdp[0] + rightdp[0];
- 对于
-
dp数组初始化
dp
数组是在每次递归调用时初始化为{0, 0}
,对于空节点,直接返回{0, 0}
,表示无节点无收益。
-
确定遍历顺序
- 由于需要从子节点信息计算父节点的最优解,因此我们使用后序遍历(左->右->根)的方式来实现。
-
举例推导dp数组
-
以示例一为例,输入: root = [3,2,3,null,3,null,1],手动推导dp状态图如下:
-
代码实现
测试地址:https://leetcode.cn/problems/house-robber-iii/
class Solution {
public:
// rotTree函数返回一个包含两个元素的数组,
// 第一个元素表示不偷当前节点能获得的最大收益,
// 第二个元素表示偷当前节点能获得的最大收益。
vector<int> rotTree(TreeNode *cur) {
// 如果当前节点为空,返回{0, 0}表示无收益
if (cur == nullptr)
return {0, 0};
vector<int> leftdp = {0, 0}; // 存储左子树的最大收益
vector<int> rightdp = {0, 0}; // 存储右子树的最大收益
if (cur->left) {
leftdp = rotTree(cur->left); // 递归计算左子树的最大收益
}
if (cur->right) {
rightdp = rotTree(cur->right); // 递归计算右子树的最大收益
}
// 不偷当前节点时的最大收益,等于左右子树的最大收益之和
int val0 = max(leftdp[0], leftdp[1]) + max(rightdp[0], rightdp[1]);
// 偷当前节点时的最大收益,等于当前节点的值加上左右子树不偷时的最大收益
int val1 = cur->val + leftdp[0] + rightdp[0];
return {val0, val1};
}
// rob函数是公开的接口,返回偷窃整棵树的最大收益
int rob(TreeNode *root) {
vector<int> result = rotTree(root); // 计算整棵树的最大收益
return max(result[0], result[1]); // 返回最大值
}
};