本篇是股票买卖问题的更多题解,在上一篇文章中我们已经介绍了这一题型,实际上是一类dp
问题,我们用自动机的思想去解决,在上一篇中我们以一道限定只买卖一次股票的题目为例进行了讲解,文章链接。下面我们继续完成剩下的题目。
1.可以多次买卖的股票买卖问题
本题链接:122.买卖股票的最佳时机II(简单)
题目
给定一个数组,它的第 i
个元素是一支给定股票第 i
天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 :
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
分析
和上一篇题目的唯一区别就是不再限制只允许买一次,因此我们只需改变一个位置即可。回忆上一篇的题目,我们是这样递推的:
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,-prices[i]);
dp0
代表今天不持有股票获得的最大利润,dp1
代表今天持有股票获得的最大利润。我们是这样总结出上面的状态转移方程的:今天不持有股票,对应两种情况:前一天就不持有股票、前一天持有股票但今天卖出了,在这两种情况中取最大值,即max(dp0,dp1+prices[i])
。今天持有股票也对应两种情况:前一天就持有股票、前一天不持有股票但是今天买入了,在这两种情况中取最大值,即max(dp1,-prices[i])
。
在上题中,由于只能买卖一次,因此在今天持有股票的“前一天不持有但今天买入”情况下只需要计算本次买入股票的花费即可,跟之前的状态无关(之前不可能买卖过股票,也就是一直在观望,利润没有发生变化),而本题则不再是这样,在本次购买之前可能已经发生过买卖了,这就是与例题唯一的不同之处。我们只需要更改这个位置为以下代码即可:
int dp0_tmp=dp0;// 记录上一次不持有股票的情况。
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,dp0_tmp-prices[i]);
参考代码
class Solution {
public int maxProfit(int[] prices) {
int dp0=0;
int dp1=-prices[0];
for(int i=1;i<prices.length;i++){
int tmp=dp0;
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,tmp-prices[i]);
}
return dp0;
}
}
2.限制最多买卖两次的股票买卖问题
本题链接:123.买卖股票的最佳时机III(困难)
题目
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 :
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
分析
现在要求只能完成两笔交易了,这就跟以前大不相同了,以前我们要么是只能买一次,要么是可以买无限次,而现在限制某一个具体数字了,我们就不得不把这个次数也考虑进状态转移方程了。我们在原来的dp
数组中再加入一个维度,记录当前的已经买卖次数。
因为现在
k
也是一个限制我们买卖股票的因素了,因为当你买卖超过两次后就不能再买入了,所以要把这个状态也算进去,天数、当前允许交易的次数、当前股票的持有状态这三个变化的因素我们都要考虑进去,所以使用**三维数组dp
**来记录。(当然,后面我们会优化成几个变量的形式,现在这样说只是为了更好理解)
举例:dp[i][k][0]
表示第i
天时不持有股票,最多还能进行k
次交易,(这里买入并卖出看作一次交易,如果买入了则必然要卖出,所以我们以买入时对k
做减一操作,卖出时就不用重复考虑了)
本题中k是从2开始取,因此这个维度只会出现2、1两个值(为0时则代表什么都不能做了,不会对获利产生影响,因此省略不写即可),我们的状态转移方程如下:
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]);
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]);
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]);
dp[i][1][1] = max(dp[i-1][1][1], -prices[i]);
前面已经说明了,我们在买入股票时做k
的更新,因此dp[i][2][1]
即“今天持有股票,还能最多买两次股票”
这个状态下的最大利润,在“前一天就持有股票”
和“前一天未持有股票,但今天买入了股票(做k-1,k变为1)”
这两个状态下利润较大值中产生。
和上一篇介绍的一样,我们到这里就发现当前每个状态事实上只与上一天的状态相关,我们没有必要用数组去记录完整的数据,我们只需要变量做缓存、每次更新递推求解即可。最终代码如下。
参考代码
class Solution {
public int maxProfit(int[] prices) {
// 初始值
int dp2_0=0;// 当前不持有股票,利润是0
int dp1_0=0;
int dp2_1=-prices[0];// 当前持有了股票,利润是-prices[0]
int dp1_1=-prices[0];
for(int i=1;i<prices.length;i++){
dp2_0=Math.max(dp2_0,dp2_1+prices[i]);// 可以rest或卖出(卖出后加股票价格的利润)
dp2_1=Math.max(dp2_1,dp1_0-prices[i]);// 可以rest或买入(买入k-1,减股票价格的花费)
dp1_0=Math.max(dp1_0,dp1_1+prices[i]);// 可以rest或卖出(卖出后加股票价格的利润)
dp1_1=Math.max(dp1_1,-prices[i]);// 可以rest或买入(注意这时k=1说明只能买入这一次,因此既然本次要买入,在本次购买前不可能发生过交易,所以本次利润直接等于股票价格的花费)
}
return dp2_0;
}
}
3.可以买卖k次的股票买卖问题
本题链接:188.买卖股票的最佳时机IV(困难)
题目
给定一个整数数组 prices
,它的第 i
个元素 prices[i]
是一支给定的股票在第 i
天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k
笔交易。(0 <= k <= 100
)
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
分析
这是这个系列中最难的一道了,在本题中我们基于上一道题k=2
的情况,拓展到**k
为任意值**的情况,因为k
可以有很多值,使得我们不能通过变量代替数组了,而且也要考虑以下k
本身的合理范围,如果题目的prices
数组长度只有10
,即十天,k
的值为100
,我们显然根本不能交易这么多次,更不能为此开辟一个100
的数组空间,所以我们要做一些限制,然后要针对每个k
值都进行一次dp
数组的递推。
像上面所说,我们先来思考一下k
的合理性如何判断。对于一个长度为len
的prices
交易天数数组,每次交易(买入、卖出)需要花费两天时间,每天最多只能进行买入、卖出中的一种,因此对于len
天来说,最多只能完成len/2
笔交易,如果给的k
值大于len/2
,那么就相当于无限次买卖了,退化成本篇第一题的情况了。对这种情况我们单独按第一题那样处理。
对于其他情况我们就只好乖乖的使用dp
数组了:(因为我们的k
这个维度是从下标1
开始用的,因此new
数组时我们把长度指定为k+1
)
int[][][] dp = new int[n][k + 1][2];
因为k
的值是任意的,我们不能直接把k=1、2
这样列举出来了,使用for
循环来遍历k
为不同值的情况:
for (int k = max_k; k >= 1; k--) {
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); }
将k
从给出的值(为区分,用max_k
代表给出的值,即最大允许的交易次数)递减循环,根据每天(第i天)
的不同已经买卖次数k
进行递推。
当然我们也要处理一下第一天的初始状态,无论k
为何值,第一天未持有股票一定利润为0
、持有股票一定利润为第一天股票价格的相反数
。即:dp[i][k][0] =0;
dp[i][k][1] =-prices[i];
最值产生在最后一天最多允许k
次交易后的不持有股票状态。为什么不是持有股票状态?前面已经说过了,如果你手里有股票,在最后一天也不抛售出去就无法赚到钱,肯定是要卖出去的。
参考代码
class Solution {
public int maxProfit(int max_k, int[] prices) {
int len;
if((len=prices.length)==0){
return 0;
}
// k值大于len/2,相当于无限制
if(max_k>len/2){
int dp0=0;
int dp1=-prices[0];
for(int i=1;i<prices.length;i++){
int tmp=dp0;
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,dp0-prices[i]);
}
return dp0;
}
// 其他情况,使用三维数组记录天数、允许交易次数、持有状态
int[][][] dp = new int[len][max_k + 1][2];
for (int k = max_k; k >= 1; k--) {
if (i == 0) { // 第一天时的初始值
dp[i][k][0] =0;
dp[i][k][1] =-prices[i];
continue;
}
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[len - 1][max_k][0];
}
}
4.含冷冻期的股票买卖问题
本题链接:309.最佳买卖股票时机含冷冻期(中等)
题目
给定一个整数数组,其中第 i
个元素代表了第 i
天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天
)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
分析
这个题比较简单,相当于是在无限制交易的基础上多加了一个“冷冻期”的概念,即卖出股票后必须休息一天才能买入,我们只需根据这个特点对状态转移方程进行一下修改即可。回想我们前面第一题的状态转移方程:
int dp0_tmp=dp0;// 记录上一次不持有股票的情况。
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,dp0_tmp-prices[i]);
dp1
的解释是:如果当天持有股票,可以是前一天就持有股票,今天休息了;也可以是前一天
没有股票,今天买入了。而在本题中,买入要和卖出间隔一天,因此第二句话要改为前两天
没有股票,今天买入了,即max(dp1,tmp-prices[i])
(用tmp
代表前二天的最大利润,当前是第i
天,前二天即第i-2
天)。然后在循环前指定一下第i-2
天的初值:0
,每次在循环中更新为dp0_tmp
的值,这样就把前二天这个事给表示出来了。
可能说的有点乱,稍微总结一下:本题关键就是把第i-2
天即tmp
的最大利润正确的更新。借助dp0_tmp
每次记录dp0
的旧值(得到第i-1
天的最大利润), tmp
每次记录dp0_tmp
的旧值(得到第i-2天的最大利润)。
参考代码
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0){
return 0;
}
int dp0=0,dp1=-prices[0],tmp=0;
// dp0代表第i天不持有股票,dp1代表第i天持有股票,tmp代表第i-2天不持有股票
// 关键则是把tmp正确的更新 借助dp0_tmp每次记录dp0的旧值(第i-1天) tmp每次记录dp0_tmp的旧值(第i-2天)
for(int i=1;i<prices.length;i++){
int dp0_tmp=dp0;
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,tmp-prices[i]);
tmp=dp0_tmp;
}
return dp0;
}
}
5.含手续费的股票买卖问题
题目
给定一个整数数组 prices
,其中第 i
个元素代表了第 i
天的股票价格 ;非负整数 fee
代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 :
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
分析
仔细阅读题目我们其实发现,这道题和不限制交易次数没有任何转移方程上的变化,只是说每次交易多交纳一些费用,我们可以等价的看成每笔买入费用增加了fee
,或者是每次卖出时少卖fee
,这里选择第一种。即每次买入时都需要花费prices[i]+fee
。
参考代码
class Solution {
public int maxProfit(int[] prices, int fee) {
int dp0=0;
int dp1=-prices[0]-fee;// 多交手续费
for(int i=1;i<prices.length;i++){
int dp0_tmp=dp0;
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,dp0_tmp-prices[i]-fee);// 多交手续费
}
return dp0;
}
}
刷题总结
到此我们已经刷完了力扣股票买卖问题了,这类dp
问题的核心其实就是找到状态、然后写出状态转移方程进行递推求解。最后再总结一下我们的dp
数组的含义:
dp[i][k][0]
:第i
天,当前最多允许交易k
次,现在手上没有股票;
dp[i][k][1]
:第i
天,当前最多允许交易k
次,现在手上持有股票;
当k
为1
或无穷大时,我们可以直接省略掉k
,当k
为2
时也可以把k=1
和k=2
简单列举出来,这些情况都可以省去数组的空间,使用不同名称的变量即可,以第二题为例:
dp2_0=Math.max(dp2_0,dp2_1+prices[i])
dp2_1=Math.max(dp2_1,dp1_0-prices[i])
dp1_0=Math.max(dp1_0,dp1_1+prices[i])
dp1_1=Math.max(dp1_1,-prices[i])
而当k
为任意值时就必须使用for
循环去在每一天的循环中更新dp
数组了。如第三题:
for (int k = max_k; k >= 1; k--) {
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); }
找好状态转移方程后,我们还要明确题目要求的最大利润在哪里产生。既然我们用自动机状态的方式去解决了,最值一定在最后一天的某个状态(终态)下产生,而且最后卖出股票才能获得股票具有的价值,因此持有状态应该是不持有。即dp[prices.length-1][k][0]
。
更多文章:
你的喜欢是我创作的动力,喜欢请关注,感谢每一个喜欢~
如有问题欢迎进行交流~
水平所限,如有错误请海涵,欢迎指正~