打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
- 示例 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 。
提示:
- 0 <= nums.length <= 100
- 0 <= nums[i] <= 400
按照题意,很简单,跳着选呗,以j += 2的方式遍历nums数组,最后找到最大值即可;
1.dp数组下标及其含义:
dp[i]考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i];要返回的值即是dp[nums.size()];
2.考虑递推公式:
dp[i] = max{dp[i-2]+nums[i], dp[i-1]};//偷i和不偷i
3.dp数组初始化:
由于公式涉及到dp[i - 2],很显然dp[0],dp[1]都需要初始化;
dp[0] = nums[0];
dp[1] = max{nums[0], nums[1]};
dp[i]是全部由前面的状态推导出来,其此处可以初始位任何值
4.确定遍历顺序
for(int i = 2; i < nums.size(); i++){
dp[i] = max{dp[i - 2] + nums[i], dp[i - 1]};
}
5.打印dp数组:
class Solution {
public:
int rob(vector<int>& nums) {
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] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
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 = [0]
- 输出:0
提示:
- 1 <= nums.length <= 100
- 0 <= nums[i] <= 1000
和上面的题的区别其实很小,就是将数组成环了,首尾也不能同时选取;
那么就是三种情况:
考虑首元素,考虑尾元素,首尾元素都不考虑;
1.[0 , nums.size() - 2] 2.[1, nums.size() - 1] 3.[1, nums.size() - 2]
注意此处只是考虑范围,并非一定选取首尾元素;
所以情况3实际上被情况1,2包含;
代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
int res1 = subRob(nums, 0, nums.size() - 2);
int res2 = subRob(nums, 1, nums.size() - 1);
return max(res1, res2);
}
int subRob(vector<int>& nums, int start, int end){
if(start == end) return nums[start];
vector<int> dp(nums.size(), 0);
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];
}
};
打家劫舍Ⅲ
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
其实和之前的那道安摄像机的题有点像的,由于题目要求不能同时取两个直接相连的房间,所以相当于是跳层遍历;
直观的暴力解法直接想到层序遍历或者直接在树形结构上进行操作,方便简单,但是暴力解法是必然会带来超时的;
考虑动态规划,此处涉及到的就是树形dp;
由于要得到的是最大值,所以比较过程应该是max(root->left->value + root->right->value, root->value);
那遍历顺序很自然就是左右中,即后序遍历;且此处可以分别用两个状态对应两种取法,那dp数组就定义为一个一维的长度为2的数组;
考虑动态规划思路分析递归三部曲:
1.确定递归函数参数和返回值
vector<int> traversal(TreeNode* cur){
//这里的返回数组就是dp数组
//所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
}
2.确定终止条件:
遇到空节点则立即返回,这里也变相等同于dp数组的初始化;
if(cur == NULL) return vector<int>{0,0};
3.遍历顺序:
//后序遍历
//0表示不拾取,1表示拾取
vector<int> leftdp = traversal(cur->left);
vector<int> rightdp = traversal(cur->right);
4.确定单层递归的逻辑
int val1 = cur->value + leftdp[0] + rightdp[0];//偷
int val2 = max(leftdp[0], leftdp[1]) + max(rightdp[0], rightdp[1]);//不偷
//这里这个取max的过程是理解整个树形结构的关键,如果不懂可以自己举例画一画过程
return vector<int>{val2, val1};
5.打印dp数组:
整体代码如下:
class Solution{
public:
int rob(TreeNode* root){
vector<int> temp = traversal(root);
return max(temp[0], temp[1]);
}
vector<int> traversal(TreeNode* cur){
if(cur == NULL) return vector<int>{0, 0};
vector<int> leftdp = traversal(cur->left);
vector<int> rightdp = traversal(cur->right);
int val1 = cur->val + leftdp[0] + rightdp[0];
int val2 = max(leftdp[0], leftdp[1]) + max(rightdp[0], rightdp[1]);
return vector<int>{val2, val1};
}
};