LeetCode198.打家劫舍&面试题17.16.按摩师(C++)
前言
两个换汤不换药的问题,用「动态规划」解决,通过一步步优化,可以用O(1)的空间复杂度来实现。
LeetCode198.打家劫舍
问题描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,
影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,
一夜之内能够偷窃到的最高金额。
示例
|| 输入:[1,2,3,1]
|| 输出:4
|| 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
解题思路1
当小偷到达一家时,他要决定偷还是不偷,那么就只有两种情况。
1.决定要偷这家,那么必须没有偷前一家,偷完这家,手里的金额等于这家的金额 nums[i] 加上没偷前一家的情况下兜里已经有的金额。
2.决定不偷这家,那么偷了或者没偷前一家都可以,我们取较大的那个金额即可。
我们利用动态规划来解决,申请一个二维数组 dp
初始状态:
1.dp[0][0] = 0 表示没偷第一家;
2.dp[0][1] = nums[0] 表示偷了第一家,小偷此时兜里的金额为从第一家偷到的金额 num[0];
状态定义:
1.dp[i][0] 代表小偷决定不偷第 i+1 家;
2.dp[i][1] 代表小偷决定偷第 i+1 家。
状态转移方程为:
dp[i][0] = max(dp[i-1][0] , dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
代码1
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0)
return 0;
int len = nums.size();
vector<vector<int>> dp(len,vector<int>(2,0));
dp[0][0] = 0;
dp[0][1] = nums[0];
for(int i=1;i<len;++i){
dp[i][0] = max(dp[i-1][0] , dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
}
return max(dp[len-1][0],dp[len-1][1]);
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
解题思路2
空间优化,利用一维数组来记录过程量。
状态定义:
dp[i] 表示到第 i 家,小偷兜里已有的金额。
初始状态:
dp[0] = 0;
dp[1] = nums[0];
状态转移方程:
dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
dp[i-1] 表示小偷已经偷了第 i-1 家,第 i 家不能偷了,金额就等于偷完第 i-1 家小偷兜里已有的金额;
dp[i-2]+nums[i-1] 表示小偷没有偷第 i-1 家且决定偷第 i 家,那么偷完第 i 家,小偷现有的金额为偷完第 i-2 家兜里总共有的金额加上能从这家偷到的金额;
两者选一个较大值作为 dp[i] 的值。
代码2
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0)
return 0;
int len = nums.size();
vector<int> dp(len+1,0);
dp[0] = 0;
dp[1] = nums[0];
for(int i=2;i<len+1;++i){
dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[len];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
解题思路3
空间优化,利用常数来记录过程量。
当小偷到达一家时,他要决定偷还是不偷,那么就只有两种情况。
1.决定要偷这家,那么必须没有偷前一家,偷完这家,手里的金额 dp1 等于这家的金额 nums[i] 加上没偷前一家的情况下兜里已经有的金额。
2.决定不偷这家,那么偷了或者没偷前一家都可以,我们让 dp0 等于较大的那个金额即可。
我们有两个初始状态:
dp0 = 0 表示没偷第一家;
dp1 = num[0] 表示偷了第一家,那小偷此时兜里的金额为从第一家偷到的金额 num[0]。
i=0 表示第一家,我们从 i=1 开始循环,表示从第二家开始做决定;
首先用 temp 暂时存储偷或不偷前一家的情况下兜里总共能有的最大金额;
先更新 dp1 再更新 dp0;
小偷来到最后一家,
偷了这家,兜里的金额是 dp1;
没偷的话,兜里的钱是 dp0;
返回其中较大的即可。
代码3
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0)
return 0;
int len = nums.size();
int dp0 = 0; //表示没偷第一家
int dp1 = nums[0];//表示偷了第一家
for(int i=1;i<len;++i){ //i=0表示第一家,从第二家开始
int temp = max(dp0 , dp1); //暂存没偷和偷了前一家中间较大的那个金额
dp1 = dp0 + nums[i]; //这家要偷,那必须没偷前一家
dp0 = temp; //这家不偷,那偷或不偷前一家都行,选较大的金额
}
return max(dp0,dp1);
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
面试题17.16.按摩师
问题描述
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。
在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。
给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
示例
|| 输入:[1,2,3,1]
|| 输出:4
|| 解释:选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。
解题思路
跟上一题一样,换汤不换药,直接用最后一种方法。
当按摩师收到一个预约请求时,她要决定接还是不接,那么就只有两种情况。
1.决定要接这个预约,那么必须没有接前一个预约,接完这个预约,按摩师此时已有的预约时长 dp1 等于这次预约的时长 nums[i] 加上没接受前一个预约的情况下已有的预约总时长。
2.决定不接这个预约,那么接或者不接前一个预约都可以,我们让 dp0 等于较大的预约时长即可。
我们有两个初始状态:
dp0 = 0 表示不接受第一个预约;
dp1 = num[0] 表示接受第一个预约,那按摩师此时预约总时长为第一个预约的时长 num[0]。
i=0 表示第一个预约,我们从 i=1 开始循环,表示从第二个预约开始做决定;
首先用 temp 暂时存储接或者不接前一个预约的情况下按摩师已有的预约总时长;
先更新 dp1 再更新 dp0;
一直到按摩师处理最后一个预约,
接了这个预约,按摩师已有的预约总时长是 dp1;
没接的话,按摩师已有的预约总时长是 dp0;
返回其中较大的即可。
代码
class Solution {
public:
int massage(vector<int>& nums) {
if(nums.size()==0)
return 0;
int len = nums.size();
int dp0 = 0; //表示不接受第一个预约
int dp1 = nums[0];//表示接受第一个预约
for(int i=1;i<len;++i){ //i=0表示第一个预约,从第二个预约开始
int temp = max(dp0 , dp1); //暂存接或者不接前一个预约时中间较大的那个时间长度
dp1 = dp0 + nums[i]; //要接这个预约,那必须没接前一个预约
dp0 = temp; //不接这个预约,那接或者不接前一个预约都行,选较大的时长
}
return max(dp0,dp1);
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)