目录
题目特点概述
力扣上有这些题:都是与股票买卖相关的题目,具有特别强的关联性与递推性,并且可以用一个方法模板解决,因此今天就来详细通俗易懂的讲解这种方法。
121.买卖股票的最佳时机
对于121题的解法:
看上去不需要使用动规,且有很简单易懂的解法。但是为了给后续做铺垫,此处也使用动规的解法去分析和解题。
定义持有股票和不持有股票的两种状态,加入将持有股票和不持有股票都用一个数组dp来表示,可读性差一点,所以可以替换为使用have数组和no数组,分别代表持有和不持有:
对于121题来说,限制交易次数为一次,故代码如下:
那么对于第i天,不持有股票的状态则有两种可能,一种是不做任何操作,一种是之前买了今天卖了,那么即:
no[i]=max(no[i-1],have[i-1]+prices[i]);
对于第i天,持有股票的最大利润,有两种可能,一种是延续之前的买了的状态,或者昨天没买,今天买了。
即 :
have[i]=max(have[i-1],-prices[i]);
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len=prices.size();
vector<int> have(len+1);
vector<int> no(len+1);
have[0]=-prices[0];
no[0]=0;
for(int i=1;i<len;i++){
have[i]=max(have[i-1],-prices[i]);
no[i]=max(have[i-1]+prices[i],no[i-1]);
}
return no[len-1];
}
};
122.买卖股票的最佳时机 II
在122题中,不限制交易次数,那么122题相比121题的代码会有什么改变呢?
改变在于:
由于有多次交易的机会,那么
have[i]=max(have[i-1],-prices[i]);
这个式子要改为:
have[i]=max(have[i-1],no[i-1]-prices[i]);
代表的意思是,我第i天的最大利润,要么是前面持有股票下得到的最大利润,要么是昨天没买今天买的利润,但是还要加上以前赚的利润。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len=prices.size();
vector<int> have(len+1);
vector<int> no(len+1);
have[0]=-prices[0];
no[0]=0;
for(int i=1;i<len;i++){
have[i]=max(have[i-1],no[i-1]-prices[i]);
no[i]=max(have[i-1]+prices[i],no[i-1]);
}
return no[len-1];
}
};
123. 买卖股票的最佳时机 III
假设限制了交易次数,例如123和188。
对于123题和188题其实很像,只是188题把123题的限定次数为2改成了限定次数k,在188中,k是一个动态的传入的参数。
我们在原来的数组上新增一个维度,将其从一维变为二维,新增的维度就是天数!
回想一下,前面我们可以计算出只交易一次得到的最优解,那我们能不能在这个结果的基础上继续计算如果交易两次得到的最优解会是什么样的?
假如题目需要我们计算3次交易所能获得的最大利润,其实是让我们依次计算,只交易一次,在不同天数下能获得的最大利润。然后基于这个只交易一次的情况下,我们就可以计算交易两次所能获得的最大利润。
计算出了交易两次所能获得的最大利润后,就可以计算交易三次所能获得的最大利润了。
所以,我们只要新增一个维度,天数,
第0行就代表交易0次,第一天第二天第三天……的最优解,
第1行就代表交易1次,第一天第二天第三天……的最优解,
第2行就代表交易2次,第一天第二天第三天……的最优解,
交易次数的注意事项:
(只有当售卖出去才算完成交易,买入不算完成交易!)
因为买入的话,只是代表当前天数时,花最少的钱买的情况,后面随着天数的增加,可能会遇到更便宜的股票。所以我最后真正购买的股票可能并非是当前这个。
对于no数组,代表的是当前没有股票的情况,那么就是之前就没有股票,或者是之前有买过股票,今天卖出去了。
转移方程推导
具体方程推导:
i代表当前是第几笔交易!而have [i] [j],假如i是3,代表是已经完成了三笔收益的情况下,再次购买股票时的最大利润,这个是用于给下一笔交易做参考用的,因为此时i=3,代表已经交易过了3次。
而no[i][j]里的i,代表的是,今天这一天即将完成第三笔交易的最大利润。
no[i][j-1]代表的是,前j-1天已经完成了i笔交易,那么是最大利润吗?不一定,得看看假如只进行了前i-1场交易,加上今天的第i场的情况下,交易金额是多少。
于是就有了如下方程:
no[i][j]=max(have[i-1][j-1]+prices[j],no[i][j-1]);
have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
举个例子就是:
no[1][j]代表的是,进行过了一次交易,是由have[0][j-1]然后卖出股票得来的,(或者是今天过的这一天是没有用的,所以就是no[i][j-1]
于是:no[i][j]=max(have[i-1][j-1]+prices[j],no[i][j-1]);
have[1][j]代表的是,进行过了一次交易,并且此时手上还持有股票,它是由前面一次无股票的状态转移而来,也就是no[i][j-1]再减去今天买股票所花的钱,(或者是今天这一天过的没有用,所以就算have[1][j-1]
于是:have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
有了状态转移方程之后,接下来的思路就是简单的遍历表然后填表即可。
初始化注意事项
在填表之前需要初始化。
注意初始化的过程很重要, 由于dp是二维数组,对于第i个位置和第j个位置会涉及到i-1和j-1的情况,所以遍历的时候,i和j均是从1开始:也就是从绿色的地方开始。而红框部分需要初始化
如果忘记初始化则会导致产生问题。
初始化相关注意事项:
先来看表格第一行:
其中,第0次交易时,have[0][j]需要初始化为负数
第0次交易时,no[i][j]=0
再来看表格第一列:
而第1~n次交易时,have[i][0],由于是第0天,不可能进行交易,所以要初始化为一个特别小的不可能的数,设定为INT_MIN,为防止溢出,就设定为INT_MIN/2,这是为了防止有格子会从这个状态转移而来。
只要设定为一个特别小的数,那么后面的动态规划就一定不会由它产生而来。
此处补充一下,vector的构造方法:
vector<int> arr(n,0);代表声明n个数,全部为0。
而二维数组的构造如下:
vector<vector<int> > arr(rows,vector<int>(columns));
所以最终代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len=prices.size();
vector<vector<int>> have(3,vector<int> (len+1));
vector<vector<int>> no(3,vector<int> (len+1));
have[0][0]=-prices[0];
no[0][0]=0;
for(int j=1;j<len;j++){
have[0][j]=max(have[0][j-1],-prices[j]);
no[0][j]=0;
}
for(int i=1;i<=2;i++){
have[i][0]=INT_MIN/2;
no[i][0]=INT_MIN/2;
}
int maxPrice=0;
for(int i=1;i<=2;i++){
for(int j=1;j<len;j++){
no[i][j]=max(have[i-1][j-1]+prices[j],no[i][j-1]);
have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
maxPrice=max(maxPrice,no[i][j]);
}
}
return maxPrice;
}
};
那么问题来了,为什么这里是return maxPrice,而不是以前很多动态规划里面的一样,返回表格的最后一个数值,即return no[k][len-1]呢?
因为有时候不是一定要交易才是好事,
比如股票连跌,不交易反倒是好事,
比如也有可能总共就3天,但是要求你交易两次,这是不可能的事情,但是表格依然会计算,但是就得到一个错误的数值,例如:
所以应该返回的不是return no[k][len-1]。
这也是常见的一个易错点,这是因为交易次数增多并不代表一定会更优。(只有交易次数增多且不会变差的那些题的情况才可以返回表格最后一个数)
188. 买卖股票的最佳时机 IV
于是乎,对于188限制k次的情况下,只需要把k改为传入的参数k和长度/2的最小值即可。
同理,对于第一题限制次数为1,则k改为1即可。
对于第二题无限次数,实际上是次数为长度最大/2.
188题代码:
class Solution {
public:
int maxProfit(int k,vector<int>& prices) {
int len=prices.size();
vector<vector<int>> have(k+1,vector<int> (len+1));
vector<vector<int>> no(k+1,vector<int> (len+1));
have[0][0]=-prices[0];
no[0][0]=0;
for(int j=1;j<len;j++){
have[0][j]=max(have[0][j-1],-prices[j]);
no[0][j]=0;
}
for(int i=1;i<=k;i++){
have[i][0]=INT_MIN/2;
no[i][0]=INT_MIN/2;
}
int maxPrice=0;
for(int i=1;i<=k;i++){
for(int j=1;j<len;j++){
no[i][j]=max(have[i-1][j-1]+prices[j],no[i][j-1]);
have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
maxPrice=max(maxPrice,no[i][j]);
}
}
return maxPrice;
}
};
309. 最佳买卖股票时机含冷冻期
对于309题有冷冻期的这一题,事实上,我们看:
首先卖出的前一天一定是买入,因此,卖出的状态转移方程不变。
但是对于买入的状态,我们就知道说,买入(即持有)的状态只能由这两种情况转移而来:原来就持有股票,
第二种情况,不能是前一天卖了股票今天再买。不能再在这两者取最大值。
第二种情况要改为:利润的最大值只能是两天前,或者更久之前卖出了股票后里挑选最大值,这个最大值的备选项不能包括前一天。因此:
只需要简单的将:
have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
其中这个地方的下标改为即可:
have[i][j]=max(have[i][j-1],no[i][j-2]-prices[j]);
然后对于这种情况下,k的最大值取得是:
int k=(len+1)/3;
为什么不是直接除以3呢?
因为最后一次交易可以不含冷冻期,例如总时长为5天的话,可以交易两次。
但是仅仅这样还不够,这里会有一个越界问题:
于是此时就需要单独把这种情况拿出来讨论了:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len=prices.size();
int k=(len+1)/3;
vector<vector<int>> have(k+1,vector<int> (len+1));
vector<vector<int>> no(k+1,vector<int> (len+1));
have[0][0]=-prices[0];
no[0][0]=0;
for(int j=1;j<len;j++){
have[0][j]=max(have[0][j-1],-prices[j]);
no[0][j]=0;
}
for(int i=1;i<=k;i++){
have[i][0]=INT_MIN/2;
no[i][0]=INT_MIN/2;
}
int maxPrice=0;
for(int i=1;i<=k;i++){
for(int j=1;j<len;j++){
if(j==1){
no[i][j]=max(have[i-1][j-1]+prices[j],no[i][j-1]);
have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
}
else{
no[i][j]=max(have[i-1][j-1]+prices[j],no[i][j-1]);
have[i][j]=max(have[i][j-1],no[i][j-2]-prices[j]);
}
maxPrice=max(maxPrice,no[i][j]);
}
}
return maxPrice;
}
};
714. 买卖股票的最佳时机含手续费
对于714手续费那一题只需要扣除手续费即可:
于是自然而然的在上面那些题通用模板中改:
class Solution {
public:
int maxProfit(vector<int>& prices,int fee) {
int len=prices.size();
int k=len/2;
vector<vector<int>> have(k+1,vector<int> (len+1));
vector<vector<int>> no(k+1,vector<int> (len+1));
have[0][0]=-prices[0];
no[0][0]=0;
for(int j=1;j<len;j++){
have[0][j]=max(have[0][j-1],-prices[j]);
no[0][j]=0;
}
for(int i=1;i<=k;i++){
have[i][0]=INT_MIN/2;
no[i][0]=INT_MIN/2;
}
int maxPrice=0;
for(int i=1;i<=k;i++){
for(int j=1;j<len;j++){
no[i][j]=max(have[i-1][j-1]+prices[j]-fee,no[i][j-1]);
have[i][j]=max(have[i][j-1],no[i][j-1]-prices[j]);
maxPrice=max(maxPrice,no[i][j]);
}
}
return maxPrice;
}
};
但是发现会超时,为什么会超时呢?
推测是因为使用了二维数组,空间容量过大,导致的超时。
而本题也是无限交易次数,无限交易次数的话也可以直接用122题的模板改就好了,因为122是一维数组,这样不容易超时。
class Solution {
public:
int maxProfit(vector<int>& prices,int fee) {
int len=prices.size();
vector<int> have(len+1);
vector<int> no(len+1);
have[0]=-prices[0];
no[0]=0;
for(int i=1;i<len;i++){
have[i]=max(have[i-1],no[i-1]-prices[i]);
no[i]=max(have[i-1]+prices[i]-fee,no[i-1]);
}
return no[len-1];
}
};