1 按摩师
面试题 17.16. 按摩师 - 力扣(LeetCode)
本题的意思简单理解就是,如果我们接受了第 i 个预约,那么第 i -1 个预约和第 i+1 个预约我们都是无法接受的,只能至少间隔一个选择。
按照以前的经验,我们的状态表示应该为:
dp[i] 表示第 i 次预约到来之后,最大总预约时长。
那么根据此状态表示我们能够推导出状态转移方程吗?
我们知道,如果要想接受第 i 个预约,必须满足第 i-1 个预约没有接受,但是从我们的状态表示中是无法得出每一天是否接受预约的,在每一个预约到来之后,按摩师都有选和不选两种状态,所以我们需要记录这两种状态下各自的最大预约时长。
f[i] 表示第 i 个预约到来之后,我们接受第 i 个预约,最大的总预约时长。
g[i] 表示第 i 个预约到来之后,我们没有接受第 i 个预约,最大的总预约时长。
那么我们可以推导出状态转移方程:
对于 f[i] ,我们想要接受第 i 个预约,我们第 i-1 个预约必须拒绝,那么我们的最大总预约时长就是第i-1个预约到来之后,拒绝第 i-1 次预约的情况下所能拿到的最大总预约时长再加上第i次预约的时长,也就是 f[i] = g[i-1] + nums[i] 。
而对于g[i],我们不想要接受第 i 个预约,那么其实就没什么前提条件了,因为题目并没有说不能连续拒绝两个预约,我们只需要保证最大的总预约时长就行了,既然不接受第 i 个预约,那么g[i] 就是在第 i-1 个预约到来之后,最大的预约时长,对于第i-1个预约可以接受也可以不接受,看哪种情况的总预约时长最大就行了,所以 g[i] = max(f[i-1],g[i-1]) 。
那么综合而言,我们的状态转移方程:
细节问题:
要注意,题目并没有给出预约个数的范围,说明有可能nums为空数组,那么这时候我们返回 0 就行了。
初始化,因为我们填表的时候要用到 f[i-1] 和 g[i-1] ,意味着 i 为0 的时候会出现越界,所以我们需要初始化 f[0] 和 g[0] 的值,f[0] = nums[0] ,因为他要接受第 0 个预约,而g[0] = 0 ,因为他不接受第i个预约。
而填表顺序是从左往右填表,f 表 和 g 表一起填。因为 f[i] 要用到 g[i-1] ,g[i] 要用到 f[i-1] 和 g[i-1] ,所以我们的两个表必须一起填,不能先填完一个再去填另一个。
返回值:我们需要返回的是最大总预约时长,那么返回值就是两种情况的最大值,max(f[n-1],g[n-1]) ,n为预约的总个数。
class Solution {
public:
int massage(vector<int>& nums) {
int n = nums.size();
if(n==0) return 0;
vector<int> f(n) , g(n);
//初始化
f[0] = nums[0];
for(int i = 1; i < n; ++i)
{
f[i] = g[i-1] + nums[i];
g[i] = max(f[i-1],g[i-1]);
}
return max(f[n-1],g[n-1]);
}
};
2 打家劫舍
LCR 089. 打家劫舍 - 力扣(LeetCode)
最简单的打家劫舍问题其实跟上面的按摩师的问题是一模一样的,其实就是换了一种说法,我们可以套用上一个题的状态表示
f[i] 表示小偷经过第 i 个房屋的时候,偷窃第 i 个房屋,能窃取的最高金额。
g[i] 表示小偷经过第 i 个房屋的时候,不偷窃第 i 个房屋,能窃取的最高金额。
那么对于 f[i] ,也就是小偷去偷窃第 i 个房屋,他的前提条件就是不能偷窃第 i-1 个房屋,那么偷完这个房间之后,窃取的最高金额就是第 i 个房子的现金,加上前面在不窃取第 i-1 个房子的情况下所窃取的最大金额,也就是 f[i] = g[i-1] + nums[i]
而对于 g[i] ,小偷不进去第 i 个房子偷窃,那么小偷在经过这个房子之后的盗窃总金额其实没有变,那么最大窃取金额就是经过前一个房子之后所能窃取的最大金额,g[i] = max(f[i-1] , g[i-1])
细节:
这个题目指明了nums的长度至少为 1,所以不用考虑nums为空的情况。
初始化,会用到 f[i-1] 和 g[i-1] ,那么当 i = 0 的时候会越界,所以我们需要手动初始化f[0] 和 g[0]。f[0] = nums[0] ,因为f[0]表示盗窃第0 个房子, g[0] =0 ,因为g[0]表示不盗窃第0个房子。
填表顺序:从左往右,两个表一起填。
返回值就是小偷走完 n 个房子之后的最大窃取金额,最后一个房子不一定窃取或者不窃取,最大金额在这两种情况下都有可能,所以我们需要返回 max(f[n-1] , g[n-1])
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> f(n),g(n);
f[0] = nums[0];
for(int i = 1; i < n; ++i)
{
f[i] = g[i-1] + nums[i];
g[i] = max(g[i - 1],f[i - 1]);
}
return max(f[n-1],g[n-1]);
}
};
3 打家劫舍Ⅱ
LCR 090. 打家劫舍 II - 力扣(LeetCode)
这个题就是在打家劫舍的基础上加了一个条件,最后一个房子和第一个房子是联通的,那么就意味着如果我们盗窃了第一个房子,那么就不能盗窃最后一个房子了。
其实我们只需要在上面的基础上,限制偷盗了第 0 个房子,就不能偷盗第 n-1 个房子就行了。
当我们选择偷窃第 0 个房子的时候,第 1 个房子和第 n-1 个房子都不能偷了,那么其实我们可以先不看第 1 和 第 n-1 个房子,我们现在 [2,n-2] 这个区间内做一次正常的打家劫舍,然后再加上第 0 个房子盗窃的金额,就是我们盗窃第 0 个房子时能窃取的最大金额。
而如果我们不盗窃第 0 个房子,那么就相当于在[1,n-1] 这个区间内做一次正常的打家劫舍,不需要考虑最后一个房子和第一个房子相连的问题。
第一轮动态规划我们盗窃第0个房子,就直接从 [2,n-2] 的区间做一次正常的打家劫舍,最终的结果就是 max( f[n-1] , g[n-2])+nums[0] 。
第二轮动态规划我们不盗窃第0个房子,就在[1,n-1]区间做一次正常的打家劫舍,最终的结果是max(f[n-1] , g[n-1] ) 。
而最终的返回值就是这两种情况下的最大金额的较大值。
而由于我们在第一种情况下,要求最少有三个房子,那么我们可以特殊处理一下只有一个房子和只有两个房子的情况。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> f(n),g(n);
//特殊处理
if(n == 1) return nums[0];
if(n == 2) return max(nums[0],nums[1]);
//第一轮,盗窃第 0 个房子,在[2,n-2]区间进行正常的打家劫舍
f[2] = nums[2]; //那么需要初始化起始位置 f[2] 和 g[2]
g[2] = 0;
for(int i = 3 ; i < n - 1 ; ++i)
{
f[i] = g[i - 1] + nums[i];
g[i] = max(f[i - 1],g[i - 1]);
}
int res = max(f[n-2],g[n-2]) + nums[0];
//第二轮,不盗窃第 0 个房子,在[1,n-1]区间做打家劫舍
f = vector<int>(n,0); //先重置一下两张表
g = f ;
f[1] = nums[1];
g[1] = 0 ;
for(int i = 2; i < n ; ++i)
{
f[i] = g[i-1] + nums[i];
g[i] = max(f[i-1],g[i-1]);
}
res = max(res,max(f[n-1],g[n-1])); //返回最大金额
return res;
}
};
4 删除并获得点数
740. 删除并获得点数 - 力扣(LeetCode)
题目意思就是,我们如果要获取整数数组中所有值为 x 的分数时,那么在获取到这些分数的时候,必须删除所有的 x-1 和 x+1 这些整数,其实就是说,如果你想要选择 x ,那么就不能选择 x-1 和 x+1这两个值。
对于每一个在数组中的 x ,有两种状态,选或者不选,那么我们需要根据这两种状态来确定状态表示。
f[i] 表示选择值为 i 的分数,所能获得的最大点数。
g[i] 表示不选择值为 i 的分数时,所能获得的最大点数。
而题目给的nums[i] 的范围是 [1,10^4],也就是说我们两个表都需要开 10^4 +1 个空间。
但是并不是在这个范围内的每一个数都出现在数组中,也不是说每个数最多只出现一次,那么我们在计算 f[i] 的时候,难道都要去遍历整个数组判断数组中有没有和有几个 i 吗?这样一来我们遍历数组的次数就会很多,那么效率就会很低。
其实我们只需要知道某个数在不在数组中和在数组中出现了几次就行了, 那么我们可以用一个哈希表来存储在数组中出现的分数以及他们出现的次数就行了。
那么分析状态转移方程:
细节:
由于会用到 i-1 的位置,所以 i = 0 的时候是会越界的,但是注意我们的状态表示f[i] 和g[i] 都是表示选择与不选择 i 的时候获得的最大分数,而 i 的范围其实我们只需要遍历 [1,10^4] 就行了,并不需要考虑 i 为 0 的情况。但是这样一来,我们就需要手动初始化 f[1] 和 g[1],那么我们不妨把多出来的这个 f[0] 和 g[0] 利用起来,可以表示选择与不选择 0 时的最大分数,既然nums[i] 中不会出现 0,那么不管选与不选 0 ,最大分数都是 0 。
填表顺序,我们需要用到前一个位置的状态,所以填表顺序为从左到右,同时填写两张表。
最后的返回值就是max(f[10000],g[10000])。
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
const int N = 10000;
//先使用一个哈希表存储每个数出现的次数
unordered_map<int,int> m;
for(auto&e:nums)
{
m[e]++;
}
//创建dp表
vector<int> f(N+1),g(N+1);
for(int i = 1 ; i <= N; ++i)
{
f[i] = g[i-1] + m[i]*i;
g[i] = max(f[i - 1],g[i - 1]);
}
return max(f[N],g[N]);
}
};
5 粉刷房子
LCR 091. 粉刷房子 - 力扣(LeetCode)
前面的多状态为体只有两个状态,而这个题目中有三个状态,其实就是每一个房子粉刷的颜色。
首先状态表示参考上面的题目,肯定就是 dp[i] 表示粉刷完第 0 ~ i 个房子的最小花费。
但是粉刷第i个房子的时候有三种选择,可以粉刷成红色、蓝色和绿色,而这三种情况都是需要考虑的,最小花费是他们之中的最小值。
那么我们可以使用三个状态表示:
f[i] 表示粉刷完前 0 ~ i 个房子,并且第i个房子粉刷成红色的最小花费
g[i] 表示粉刷完前 0 ~ i 个房子,并且第i个房子粉刷成蓝色的最小花费
h[i] 表示粉刷完前 0 ~ i 个房子,并且第i个房子粉刷成绿色的最小花费
状态转移方程分析:
对于f[i] ,我们需要将第 i 个房子粉刷成红色,而对前面 0~ i-1 个房子没有限制,由于第i个房子的花费已经固定,那么我们就需要将粉刷 0 ~ i-1 个房子的代价控制在最小,这就是我们的dp[i]。同时,由于第 i 个房子已经确定要粉刷成红色了,那么第 i - 1 个房子只能有两种情况,也就是绿色或者蓝色,要是总的花费最小,我们就需要去这两种情况的较小方案来粉刷。
同理,推导 g[i] 和 h[i] 的状态转移方程也是一样的思路
细节问题:
初始化 f[0] ,g[0],h[0] 的时候需要用到下标为 -1 的数据,所以我们需要对 f[0],g[0],h[0]进行初始化,初始化的值也很简单,无非就是将第0个房子粉刷成对应颜色的代价。
返回值就是粉刷完n个房子的最小值,而最后一个房子可以粉刷成三个颜色中的任意一个,最小花费的方案可以是这三种情况中大的任意一种,所以返回值是 min(f[n-1] , g[n-1] ,h[n-1])。
填表顺序:由于填写每一个表的第 i 项的时候,都需要用到另外两张表的第 i-1 项,所以需要确保填写到第 i 项的时候,另外两张表的第 i-1 项已经填好,所以填表顺序是三张表一起从左往右填。
那么代码如下:
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
//dp[i]表示以最小花费粉刷前[0,i-1]个房子,然后将第i个房子粉刷为某一种颜色的最小花费
int n = costs.size();
vector<int> f(n),g(n),h(n);
//初始化f[0],g[0],h[0]
f[0] = costs[0][0] , g[0] = costs[0][1] , h[0] = costs[0][2];
for(int i = 1 ; i < n ; ++i){
f[i] = costs[i][0] + min(g[i-1] , h[i-1]);
g[i] = costs[i][1] + min(f[i-1] , h[i-1]);
h[i] = costs[i][2] + min(f[i-1] , g[i-1]);
}
return min(min(f[n-1] , g[n-1]) , h[n-1]);
}
};
当然我们也可以把三个一维的dp表转换成一个二维的dp表。用dp[0][i] 表示f[i],dp[1][i]表示g[i],dp[2][i] 表示为h[i]。其实理解好了转换一下就很简单了,二维dp表代码如下:
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
//dp[i]表示以最小花费粉刷前[0,i-1]个房子,然后将第i个房子粉刷为某一种颜色的最小花费
int n = costs.size();
vector<vector<int>> dp(3,vector<int>(n));
//初始化f[0],g[0],h[0]
dp[0][0] = costs[0][0] , dp[1][0] = costs[0][1] , dp[2][0] = costs[0][2];
for(int i = 1 ; i < n ; ++i){
dp[0][i] = costs[i][0] + min(dp[1][i-1] , dp[2][i-1]);
dp[1][i] = costs[i][1] + min(dp[0][i-1] , dp[2][i-1]);
dp[2][i] = costs[i][2] + min(dp[0][i-1] , dp[1][i-1]);
}
return min(min(dp[0][n-1] , dp[1][n-1]) , dp[2][n-1]);
}
};