目录
LeetCode198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路:这里需要注意需要跳着偷,连着偷就会报警。
那么其实每一次取还是不取还是有讲究的,需要记录前面的状态。
因此很容易想到使用动归的方法。
这里先介绍使用二维数组维护的情况。
dp[i][0]表示不偷第i间房的最大价值,dp[i][1]表示偷第i间房的最大价值。那么如果说我不偷第i间房,那么到第i间房时的最大价值就和第i-1间房有关了,那第i-1间房偷还是不偷呢?其实偷不偷都行,关键我要最后价值最大,所以得在这两种情况种取最大的;那么如果我要偷第i间房,则前一间房不能被偷,否则连着偷就会报警,所以最后偷了价值为dp[i-1][0]+nums[i]。
其实还是比较好理解的,最后再比较一下最后一间房子偷还是不偷的累计价值哪个大就选哪个。
int rob(vector<int>& nums) {
//dp[i][0]表示不偷窃当前房屋的最大价值,dp[i][1]表示偷窃当前房屋的最大价值
vector<vector<int>> dp(nums.size(), vector<int>(2, 0));
dp[0][0] = 0;
dp[0][1] = nums[0];
for(int i = 1; i < nums.size(); i ++){
dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);//当当前房屋不准备偷窃的时候,那么就需要选择前面一个房屋被偷窃和不被偷窃时两者的最大值;
dp[i][1] = dp[i - 1][0] + nums[i];//当准备偷窃当前房屋时,就需要保证前一个房屋没有被偷窃,否则会触发报警
}
return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);//最后返回两者中的最大值即可
}
时间复杂度:O(n)
空间复杂度:O(n)
当然还可以使用一维数组。
这里的dp[j]表示到了第i间房后,前面所有房子能偷的最大价值。
如果偷了第i间房的物品,那么就需要看dp[i-2]的情况,然后加上nums[i],注意这里的第i-2间房子不一定被偷,但是不管偷或者不偷,dp[i-2]记录下的都是当前最大价值,这也就足够了;然后如果不偷第i间房,那么到第i间房之前的价值就是dp[i-1],并且第i-1这件房子也不一定被偷。
所以不一定dp[i]中第i间房一定会被偷,但是能保证的是dp[i]一定是递推公式推导出来的第0到i间房偷取的最大价值,只要明白这一点就可以了。
int rob(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
vector<int> dp(nums.size(), 0);//dp[i]表示到当前第i间房屋时所能偷取的最大金额
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]);//当决定要偷的时候,那么就需要考虑到dp[i-2]的情况,因为只能跳着偷;如果不偷的话就直接考虑dp[i - 1]的状态,总之需要取两者的最大值
}
return dp[nums.size() - 1];
}
时间复杂度:O(n)
空间复杂度:O(n)
LeetCode213.打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
思路:这道题相对于上面的题来说,多了一个首尾相连的情况。
于是我们可以将其分成三种情况讨论。比如有[1,2,3,4,5]
第一种,不包含首位元素,如[2,3,4];
第二种,包含首元素,不包含尾元素,如[1,2,3,4];
第三种,不包含首元素,包含尾元素,如[2,3,4,5]。
我们可以看到,第一种情况的区间其实是包含在第二、第三种情况中的。于是我们就可以分别对第二种和第三种情况进行跳选的选择机制,求取最大价值,然后比较两者,取最大即可。
这道题关键就是首和尾元素不能同时参与区间计算,将其分开正好可以保证这一要求,从而求得最大价值。
int rob_result(vector<int>& nums, int start, int end){
if(end - start == 0) 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 - 2] + nums[i], dp[i - 1]);
}
return dp[end];
}
int rob(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
int result1 = rob_result(nums, 0, nums.size() - 2);//这里是考虑前面下标从0到size-2的房子的情况,也就是不包括末尾元素
int result2 = rob_result(nums, 1, nums.size() - 1);//这里是考虑后面下标从1到size-1的房子的情况,也就是不包括首元素
return max(result1, result2);//返回两者中的最大值
}
时间复杂度:O(n)
空间复杂度:O(n)
LeetCode337.打家劫舍III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为
root
。除了
root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。给定二叉树的
root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
思路:这道题相较于前面两道题要稍微难一点了,涉及到了二叉树的知识。
这里我介绍两种方法,二叉树递归以及动态规划。
1. 二叉树递归
对于二叉树来说,首先我们要明确递归顺序。
因为我们要根据子节点的情况来对父节点选择或者不选做出判断,所以遍历顺序是后序遍历左右中。
这里我们使用了一个map数组记录从每个结点出发能够获取到的最大价值,这里之所以将状态情况记下来,是因为如果之后在遍历之前查询出来已经处理过了的话,直接使用即可,不用再递归遍历了,节省不少空间和时间的消耗,这也是记忆化递归的一个应用。
当我们选择偷父节点的时候,这时候就需要跳过父节点的左右孩子,从左孩子的左孩子、右孩子出发,以及从右孩子的右孩子、左孩子出发,这时当递归完全结束后会有一个值;当不选择偷父节点的时候,那么就可以从它的左右孩子分别出发,递归结束后又是一个值,最后二者比较取最大即可。
unordered_map<TreeNode*, int> map;//用于存储结点的状态,实现记忆化递归
int rob(TreeNode* root) {
if(root == NULL) return 0;
if(root -> left == NULL && root -> right == NULL) return root -> val;
if(map[root]) return map[root];//这里查询map,如果已经记录好状态,直接返回即可,不用再进行遍历
//当取头节点时
int val1 = root -> val;
if(root -> left) val1 += rob(root -> left -> left) + rob(root -> left -> right);//左,跳过了根节点的左孩子
if(root -> right) val1 += rob(root -> right -> left) + rob(root -> right -> right);//右,跳过了根节点的右孩子
//当不取头节点时
int val2 = rob(root -> left) + rob(root -> right);
map[root] = max(val1, val2);//这里将结点的状态记录下来,避免后序反复迭代,消耗空间和时间
return max(val1, val2);
}
时间复杂度:O(n)
空间复杂度:O(logn)
2. 动态规划
上面是再递归过程中逐步计算的价值,没有记录每个结点的状态。
而动态规划可以将各个结点的状态记录下来。
这里我们为每个结点采用了一个大小为2的数组,下标为0对应的值记录不偷该结点时的最大价值,下标为1对应的值记录偷该结点时的最大价值。
我们依然是首先采用了后序遍历的方法,找到左右子树的状态,然后处理中间结点。
如果选择了中间结点,那么左右孩子都不能偷,也就是都为下标为0的价值,累加即可;
如果没有选择中间结点,那么左右孩子不管偷还是不偷,我只要价值最大的情况,于是左孩子偷与不偷的情况取最大值,有孩子偷与不偷取最大值,最后合并一加即得没有偷中间结点的最大价值。
当然最后返回的是对于根节点的偷与不偷的情况,因为根节点下面的孩子们已经完成了计算,也就是说返回根节点的大小为2的数组即为最终结果,最后选择其中价值最大的返回即可。
vector<int> rob_Max(TreeNode* root){
//每个结点都有一个二维数组{0,0},下标为0表示不偷该结点的最大价值,下标为1表示偷取该节点的最大价值
if(root == NULL) return {0, 0};//这里相当于进行了初始化
vector<int> left = rob_Max(root -> left);//左
vector<int> right = rob_Max(root -> right);//右
//处理中间逻辑
//当选择中间结点的时候
int val1 = root -> val + left[0] + right[0];//选择偷中间结点后,两边结点是不能选择的
//当不选择中间结点的时候
int val2 = max(left[0], left[1]) + max(right[0], right[1]);//当不偷中间结点的时候,就等于两边最大值之和
return {val2, val1};
}
int rob(TreeNode* root) {
vector<int> dp = rob_Max(root);
return max(dp[0], dp[1]);
}
时间复杂度:O(n)
空间复杂度:O(logn)
感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。
如果有什么问题欢迎评论区讨论!