题目:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
当前房间是否偷依赖于前一个和前两个房间。和爬楼梯很相似,选择爬一个台阶还是两个台阶
1. 确定dp[j] dp数组以及下标的含义
考虑下标i,能偷的最大金额为dp[i]
2. 确定递推公式
偷i: dp[i-2]+nums[i] (i-2包括之前所有房间所偷的最大金币数量+偷i的金币数量,考虑到i-2不一定真的偷i-2)
不偷i: dp[i-1] (考虑前一个房间以及前一个房间之前最大金币数量,考虑到i-1不一定真的偷i-1)
dp[nums.size()-1]: 不一定偷最后一个元素,考虑最后一个元素及之前所有房间所有的最大金币数量
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
3. dp数组如何初始化
vector dp(nums.size());
dp[0] = nums[0];
// 一定得偷,考虑下标0所偷的最大钱币
dp[1] = max(nums[0], nums[1]);
// 考虑下标1以及之前房间能偷的最大数量
非0非1下标可以初始成0或者是其他值(也会被覆盖),他们是由前面状态推导而来的。
4. 确定遍历顺序
for (int i = 2; i < nums.size(); i++) { // 0,1已初始化
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
5. 打印dp数组
元素组划分成以下3种情况后,对原数组均无影响。
情况二和情况三考虑范围(最优解)都包含了情况一,不一定要选首元素。是否选首元素由递推公式决定。
因此,只要求情况二最优解和情况三最优解的最大值
class Solution {
public:
int rob(vector& nums) {
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);
}
// 198.打家劫舍的逻辑
int robRange(vector& nums, int start, int end){
if (start == end) return nums[start];
vector dp(nums.size());
dp[start] = nums[start];
dp[start+1] = max(nums[start],nums[start+1]);
for (int i=start+2; i<=end; i++){
dp[i] = max(dp[i-1], dp[i-2]+nums[i]);
}
return dp[end];
}
};
动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
1. 确定递归函数的参数和返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。
参数为当前节点,代码如下:
vector robTree(TreeNode* cur) {
其实这里的返回数组就是dp数组。
所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
2. 确定终止条件 (dp数组的初始化)
// 遇到空节点的话,很明显,无论偷还是不偷都是0
if (cur == NULL) return vector{0, 0};
3. 确定遍历顺序
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。将状态从底一层一层往上推,最终得到根节点偷or不偷所得到的最大钱币。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
// 下标0:不偷,下标1:偷
vector left = robTree(cur->left); // 左
vector right = robTree(cur->right); // 右
// 中
4. 确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱} // 系统栈保存每一层递归的参数,当前层的dp数组就表示当前层所遍历的节点的状态
vector left = robTree(cur->left); // 左
vector 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};
5. 举例推导dp数组
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int rob(TreeNode* root) {
vector result = robTree(root);
return max(result[0], result[1]);
}
// 长度为2的数组,0:不偷,1:偷
vector robTree(TreeNode* cur){
if (cur == NULL) return vector {0,0};
vector left = robTree(cur->left);
vector right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0]; // left right都是dp数组
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1])+max(right[0], right[1]);
return {val2, val1};
}
};