LeetCode No.198 打家劫舍Ⅰ && LeetCode 面试17.16 按摩师问题
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 :
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
按摩师问题是一样的就不再复制题干了。
class Solution {
public:
int massage(vector<int>& nums) {
int n = nums.size();
if(n==0) return 0;
if(n==1) return nums[0];
int dp[n];
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i = 2;i<n;i++){
dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
};
时间复杂度O(n)。
空间复杂度O(n)。
LeetCode No.198 执行时间 4 ms
状态转移方程:dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
偷还是不偷这是一个问题
偷:取dp[i-2]+nums[i]
不偷:取dp[i-1]
因为偷完之后要等待一个晚上才能继续偷,所以所有的夜晚都可以表示为第一晚、第三晚。
第一晚已经抢了一次,那么选择可以分为:
①第三晚抢nums[i]
并加入第一晚的所有金额之中dp[i-2]
②第三晚不打草惊蛇保持一如既往的金额dp[i-1]
优化:
上述代码用了n的空间,其实并不用那么大的空间就可以做出来:
class Solution {
public:
int massage(vector<int>& nums) {
int dp0 = 0;
int dp1 = 0;
for(int i = 0;i<nums.size();i++){
int dp2 = max(dp1,dp0+nums[i]);
dp0 = dp1;
dp1 = dp2;
}return dp1;
}
};
时间复杂度O(n)。
空间复杂度O(1)。
LeetCode No.213 打家劫舍Ⅱ
第二题与第一题的区别是:这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的——也就是nums[0]
和nums[n-1]
不能同时偷。
那么我们就分两次dp来计算其中的最大值!
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0) return 0;
/*👇只能偷一次的情况*/
if(n == 1) return nums[0];
if(n == 2 || n == 3) {
int res = 0;
for(int i = 0 ;i<n;i++)
res = max(res,nums[i]);
return res;
}
/*👇第一天开始偷的情况*/
int dp[n];
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2;i<n;i++){
dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
}
/*👇第二天开始偷的情况*/
int pd[n];
pd[0] = 0;
pd[1] = nums[1];
for(int i = 2;i<n;i++){
pd[i] = max(pd[i-1],pd[i-2]+nums[i]);
}
/*👇对比两种方式谁偷的多*/
return max(dp[n-1],pd[n-1]);
}
};
LeetCode No.213 执行时间 0 ms
LeetCode No.337 打家劫舍Ⅲ
第三次邻居的财产变成了二叉树,相邻的节点不能偷取——即root
和root->left
或root->right
不能同时偷取。
示例 :
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
class Solution {
public:
int rob(TreeNode* root) {
if (root == NULL)
return 0;
return fun(root,0);
}
int fun(TreeNode* root,int flag) {
if (root == NULL) return 0;
if (flag == 0) {
return max(root->val+fun(root->left,1)+fun(root->right,1),fun(root->left,0)+fun(root->right,0));
}else
return fun(root->left,0)+fun(root->right,0);
}
};
-
状态定义:
flag == 0
表示root结点不可以偷取,只能偷取下一个结点;
flag == 1
表示root结点可以偷取。 -
状态转移方程:
①fun(root,0) = max(root->val+fun(root->left,1)+fun(root->right,1) , fun(root->left,0) + fun(root->right,0))
②fun(root,1) = fun(root->left,0)+fun(root->right,0)
-
解释:偷还是不偷这是一个问题
偷=max(当前的资产
+左节点不可偷
+右节点不可偷
,左节点可偷
+右节点可偷
)
不偷=(左节点不可偷
+右节点不可偷
)
执行之后发现是可行的,但是提交之后发现超时,于是我又在java里提交了相同的代码:
class Solution {
public int rob(TreeNode root) {
if (root == null)
return 0;
return fun(root,0);
}
int fun(TreeNode root,int flag) {
if (root == null) return 0;
if (flag == 0) {
return Math.max(root.val+fun(root.left,1)+fun(root.right,1),fun(root.left,0)+fun(root.right,0));
}else
return fun(root.left,0)+fun(root.right,0);
}
}
执行用时 :2402 ms, 在所有 Java 提交中击败了5.03%的用户
内存消耗 :39.2 MB, 在所有 Java 提交中击败了35.36%的用户
(ˉ▽ˉ;)…
这好像也是在超时的边缘上。。。。。
看了leetcode评论区上的其他答案,也有和我类似的做法,但是能通过的只有java(C++和C#都不可以)。
所以我决定重新分析…
原本的代码因该是包含了一定的重复值,包含root和不包含root无论是先区分还是在最后return的时候区分结果应该是一样的,可以提升的是应该把左节点和右节点更加细致地表示出来:
class Solution {
public:
int rob(TreeNode* root) {
if(root == NULL) return 0;
int ll = 0, lr = 0, rl = 0, rr = 0;
int left, right;
left = rob(root->left);
right = rob(root->right);
if(root->left){
if(root->left->left){
ll = root->left->left->val;
}
if(root->left->right){
lr = root->left->right->val;
}
}
if(root->right){
if(root->right->left){
rl = root->right->left->val;
}
if(root->right->right){
rr = root->right->right->val;
}
}
root->val = max(root->val + ll + lr + rl + rr, left + right);
return root->val;
}
};
执行用时 :40 ms, 在所有 C++ 提交中击败了30.53%的用户
内存消耗 :22 MB, 在所有 C++ 提交中击败了63.83%的用户