198.打家劫舍
动态规划五部曲!
1、定义 dp 数组下标及值的含义
dp[i]:从下标 0 到 i 的房屋里偷,值表示从下标 0 到 i 的房屋里面偷最多可以偷到金额为 dp[i]
2、确定递推公式
要求 dp[i],需考虑从下标 0 到 i 的房屋里应该怎么偷。偷窃方案可以分成两类:该方案偷了房屋 i 和该方案没偷房屋 i
偷了房屋 i:则此时房屋 i - 1 不能偷,偷了 i 之后剩下只能在房屋 0 到 i - 2 里面尽可能多偷点,即该方案最多可以偷到的金额为 nums[i] + dp[i - 2]
没偷房屋 i:则此时相当于在房屋 0 到 i - 1 里面尽可能多偷点,即该方案最多可以偷到的金额为 dp[i - 1]
下标 0 到 i 的房屋里面偷最多可以偷到金额 dp[i] 为两类方案中的最大值,即递推公式为:dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
3、dp 数组初始化
根据递推公式可以看出,dp[i] 是通过 dp[i - 1] 和 dp[i - 2] 推导出来,因此应该初始化 dp[0] 和 dp[1]
dp[0]:从房间 0 里面最多可以偷到的金额,就是房间 0 里面有的金额,dp[0] = nums[0]
dp[1]:从房间 0 到 1 里面最多可以偷到的金额,就是房间 0 和 1 里面的金额的最大值,dp[0] = max(nums[0], nums[1])
其他位置随意初始化,反正会被覆盖
4、确定遍历顺序
当前 dp 值是由前两个 dp 值推导出来的,那么一定是从前到后遍历,保证前两个 dp 值是已被更新的正确值
5、打印 dp 数组验证
代码如下
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
// 确定dp数组下标及值含义
// dp[i]:从下标0-i的房间偷,最多能偷到的金额为dp[i]
vector<int> dp(nums.size());
// 递推公式:考虑偷房间i和不偷房间i两种偷窃方案,dp[i] = max(nums[i]+dp[i-2], dp[i-1])
// 初始化dp[0]和dp[1]
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2;i < nums.size(); ++i) { // 从左向右遍历,dp[0]和dp[1]已被初始化了
dp[i] = max(nums[i] + dp[i - 2], dp[i - 1]);
}
// 打印dp数组验证略
return dp.back();
}
};
213.打家劫舍II
本题和 198.打家劫舍 的唯一区别就是房屋成环了
成环意味着,偷了第一个房间就不能偷最后一个房间,偷了最后一个房间就不能偷第一个房间
我们分两种考虑:
第一种仅考虑除最后一个房间之外的房间,这样能够保证首尾不会都被偷
第二种仅考虑除第一个房间之外的房间,这样能够保证首尾不会都被偷
这两种都是仅考虑红框部分,则每种考虑的处理方式和 198.打家劫舍 一致,可以进行代码复用。这两种考虑的最大值就是最终结果
代码如下
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
int result1 = robRange(vector<int>(nums.begin(), nums.end() - 1)); // 仅考虑除最后一个房间之外的房间
int result2 = robRange(vector<int>(nums.begin() + 1, nums.end())); // 仅考虑除第一个房间之外的房间
return max(result1, result2);
}
int robRange(const vector<int>& nums) { // 打家劫舍I的代码
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
// 确定dp数组下标及值含义
// dp[i]:从下标0-i的房间偷,最多能偷到的金额为dp[i]
vector<int> dp(nums.size());
// 递推公式:考虑偷房间i和不偷房间i两种偷窃方案,dp[i] = max(nums[i]+dp[i-2], dp[i-1])
// 初始化dp[0]和dp[1]
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2;i < nums.size(); ++i) { // 从左向右遍历,dp[0]和dp[1]已被初始化了
dp[i] = max(nums[i] + dp[i - 2], dp[i - 1]);
}
// 打印dp数组验证略
return dp.back();
}
};
337.打家劫舍III
本题和树有关, 我们做树的题目就是递归三部曲或者回溯
本题要用递归三部曲,结合动态规划的思想
复习一下递归三部曲
递归把握一个关键点:确定这个递归调用能干什么事
递归三部曲(每一步都考虑一下那个关键点)
- 确定递归的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑(往往想到能不能利用本身递归函数解决子问题)
我们就按照递归三部曲的思路,一步一步来做这道题
首先明确递归函数功能:传入一棵树,返回在这棵树上能够偷到的最大金额
因为在一棵树上偷,有两种方案:要偷根节点的方案,及不偷根节点的方案,是否偷某棵子树的根节点与我们决策某棵子树的父节点应该怎么偷息息相关,于是我们分开考虑
因此我们递归函数功能优化为:传入一棵树,返回偷了该树根节点所能偷到的最大金额及不偷该树根节点所能偷到的最大金额
然后递归三部曲
1、 确定递归的参数和返回值
传入一棵树表示等待被偷的树,返回一个数组,数组存放的是偷了该树根节点所能偷到的最大金额及不偷该树根节点所能偷到的最大金额
vector<int> robTree(TreeNode* cur)
// 返回数组vec
// vec[0]存放的是不偷树的根节点所能偷到的最大金额
// vec[1]存放的是偷了树的根节点所能偷到的最大金额
2、 确定递归终止条件
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
if (cur == NULL) return vector<int>{0, 0};
3、 确定单层递归逻辑
单层递归是需要我们分别返回偷了该树根节点及不偷该树根节点两种方案所能偷到的最大金额
偷该树根节点所能偷到的最大金额:为根节点金额、不偷左子树根节点所能偷到的最大金额、不偷右子树根节点所能偷到的最大金额之和
不偷该树根节点所能偷到的最大金额:为在左子树中所能偷到的最大金额(可偷左子树的根也可以不偷,取最大的)、在右子树中所能偷到的最大金额之和
偷或者不偷其子树根节点所能偷到的最大金额能够通过递归调用本身实现
// 偷或者不偷其子树根节点所能偷到的最大金额能够通过递归调用本身
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
// 返回数组位置0代表不偷,位置1代表偷
return {val2, val1};
整体代码如下
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
// 长度为2的数组,0:不偷,1:偷
vector<int> robTree(TreeNode* cur) {
if (cur == NULL) return vector<int>{0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
回顾总结
最后一道题树形 dp 挺有难度
前两道就是常规动态规划题,定义好 dp,推出递推公式就很简单了
总的来说比背包问题简单太多