【力扣】买卖股票问题
零、买卖股票的最佳时机 IV
股票问题都是具有共性的,只需要抽出来力扣第 188 题「 买卖股票的最佳时机 IV」进行研究,因为这道题是最泛化的形式,其他的问题都是这个形式的简化,看下题目:
第一题是只进行一次交易,相当于 k = 1
;第二题是只进行 2 次交易,相当于 k = 2
;第三题是不限交易次数,相当于 k = +infinity
(正无穷);剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第三题的变种,都很容易处理。
下面言归正传,开始解题。
1、确定dp数组以及下标的含义
使用二维数组 dp[i][j]
:第 i
天的状态的 j
下,所剩下的最大现金是dp[i][j]
。
j
的状态表示为:
- 0 表示不操作
- 1 第一次买入
- 2 第一次卖出
- 3 第二次买入
- 4 第二次卖出
- .....
大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。
题目要求是至多有K笔交易,那么 j
的范围就定义为 2 * k + 1 就可以了。
所以二维dp
数组的定义为:
int[][] dp = new int[prices.length][2 * k + 1];
2、确定递推公式
还要强调一下:dp[i][1]
,表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。
达到 dp[i][1]
状态,有两个具体操作:
- 操作一:第
i
天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
; - 操作二:第
i
天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
;
选最大的,所以 dp[i][1] = (dp[i - 1][0] - prices[i], dp[i - 1][1])
;
同理 dp[i][2]
也有两个操作:
- 操作一:第
i
天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
; - 操作二:第
i
天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
;
所以dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2])
同理可以类比剩下的状态,代码如下:
for (int j = 0; j < 2 * k - 1; j += 2) {
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
本题和动态规划:123.买卖股票的最佳时机III (opens new window)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态。
3、dp数组如何初始化
第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0
;
第0天做第一次买入的操作,dp[0][1] = -prices[0]
;
第0天做第一次卖出的操作,这个初始值应该是多少呢?
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0
;
第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
所以第二次买入操作,初始化为:dp[0][3] = -prices[0]
;
第二次卖出初始化dp[0][4] = 0
;
所以同理可以推出 dp[0][j]
当j为奇数的时候都初始化为 -prices[0]
代码如下:
for (int j = 1; j < 2 * k; j += 2) dp[0][j] = -prices[0];
在初始化的地方同样要类比j为奇数是买、偶数是卖的状态。
4、确定遍历顺序
从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i]
,依靠dp[i - 1]
的数值。
5、举例推导 dp 数组
以输入[1,2,3,4,5],k=2为例。
最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]
即红色部分就是最后求解。
以上分析完毕,代码如下:
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length == 0) return 0;
int[][] dp = new int[prices.length][2 * k + 1];
for (int j = 1; j < 2 * k; j += 2) dp[0][j] = -prices[0];
//j+1持有, j+2不持有
for (int i = 1; i < prices.length; i++) {
for (int j = 0; j < 2 * k - 1; j += 2) {
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
}
return dp[prices.length - 1][2 * k];
}
}
- 时间复杂度: O(n * k),其中 n 为 prices 的长度
- 空间复杂度: O(n * k)
当然有的解法是定义一个三维数组dp[i][j][k]
,第 i
天,第 j
次买卖,k
表示买还是卖的状态,从定义上来讲是比较直观。
但感觉三维数组操作起来有些麻烦,我是直接用二维数组来模拟三维数组的情况,代码看起来也清爽一些。
一、买卖股票的最佳时机 I
第一题,先说力扣第 121 题「 买卖股票的最佳时机」,相当于 k = 1
的情况:
1、确定dp数组以及下标的含义
使用二维数组 dp[i][j]
:第 i
天的状态的 j
下,所剩下的最大现金是dp[i][j]
。
j
的状态表示为:
- 0 表示不操作 //这个操作是为了多次买卖时,所有操作都统一,而这里只有一次买卖,可以不需要
- 1 第一次买入
- 2 第一次卖出
1就是买入,2就是卖出。
所以二维dp
数组的定义为:
int[][] dp = new int[prices.length][2];
2、确定递推公式
还要强调一下:dp[i][0]
,表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。
达到 dp[i][1]
状态,有两个具体操作:
- 操作一:第
i
天买入股票了,那么dp[i][0] = -prices[i]
; - 操作二:第
i
天没有操作,而是沿用前一天买入的状态,即:dp[i][0] = dp[i - 1][0]
;
选最大的,所以 dp[i][0] = Math.max(-prices[i], dp[i - 1][0])
;
同理 dp[i][1]
也有两个操作:
- 操作一:第
i
天卖出股票了,那么dp[i][1] = dp[i - 1][0] + prices[i]
; - 操作二:第
i
天没有操作,沿用前一天卖出股票的状态,即:dp[i][1] = dp[i - 1][1]
;
所以dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1])
3、dp数组如何初始化
第0天持有的状态只能是,dp[0][0] = -prices[0]
;
第0天不持有的状态只能是0,dp[0][1] = 0
;
4、确定遍历顺序
从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i]
,依靠dp[i - 1]
的数值。
5、举例推导 dp 数组
以示例1,输入:[7,1,5,3,6,4]为例,dp
数组状态如下:
dp[5][1]
就是最终结果。
为什么不是dp[5][0]
呢?
因为本题中不持有股票状态所得金钱一定比持有股票状态得到的多!
以上分析完毕,代码如下:
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) return 0;
int length = prices.length;
// dp[i][0]代表第i天持有股票的最大收益
// dp[i][1]代表第i天不持有股票的最大收益
int[][] dp = new int[length][2];
int result = 0;
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
}
return dp[length - 1][1];
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
从递推公式可以看出,dp[i]
只是依赖于 dp[i - 1]的状态
。
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
二、买卖股票的最佳时机 III
第二题,看力扣第 123 题「 买卖股票的最佳时机 III」,也就是 k = 2
的情况:
1、确定dp数组以及下标的含义
使用二维数组 dp[i][j]
:第 i
天的状态的 j
下,所剩下的最大现金是dp[i][j]
。
j
的状态表示为:
- 0 表示不操作
- 1 第一次买入
- 2 第一次卖出
- 3 第二次买入
- 4 第二次卖出
大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。
题目要求是至多有K笔交易,那么 j
的范围就定义为 2 * k + 1
就可以了,由于 k=2
,所以 j=5
;
所以二维dp
数组的定义为:
int[][] dp = new int[prices.length][5];
2、确定递推公式
还要强调一下:dp[i][1]
,表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。
达到 dp[i][1]
状态,有两个具体操作:
- 操作一:第
i
天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
; - 操作二:第
i
天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
;
选最大的,所以 dp[i][1] = (dp[i - 1][0] - prices[i], dp[i - 1][1])
;
同理 dp[i][2]
也有两个操作:
- 操作一:第
i
天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
; - 操作二:第
i
天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
;
所以dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2])
同理可以类比剩下的状态,代码如下:
for (int j = 0; j < 3; j += 2) {
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
3、dp 数组如何初始化
第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0
;
第0天做第一次买入的操作,dp[0][1] = -prices[0]
;
第0天做第一次卖出的操作,这个初始值应该是多少呢?
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0
;
第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
所以第二次买入操作,初始化为:dp[0][3] = -prices[0]
;
第二次卖出初始化dp[0][4] = 0
;
代码如下:
for (int j = 1; j < 4; j += 2) dp[0][j] = -prices[0];
在初始化的地方同样要类比j为奇数是买、偶数是卖的状态。
4、确定遍历顺序
从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i]
,依靠dp[i - 1]
的数值。
5、举例推导 dp 数组
以输入[1,2,3,4,5],k=2为例。
最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]
即红色部分就是最后求解。
以上分析完毕,代码如下:
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) return 0;
int[][] dp = new int[prices.length][5];
//j+1持有, j+2不持有(卖出)
for (int j = 1; j < 4; j += 2) dp[0][j] = -prices[0];
for (int i = 1; i < prices.length; i++) {
for (int j = 0; j < 3; j += 2) {
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
}
return dp[prices.length - 1][4];
}
}
- 时间复杂度: O(n),其中 n 为 prices 的长度
- 空间复杂度: O(n )
三、买卖股票的最佳时机 II
第三题,看一下力扣第 122 题「 买卖股票的最佳时机 II」,也就是 k
为正无穷的情况:
这道题的特点在于没有给出交易总数 k
的限制,也就相当于 k
为正无穷。
k
没有限制反而更加简单,只需要记录前一天持有0和不持有(卖出)1的情况就可以了!!!
和第一题唯一不同的是,由于可能会有多次卖出,所以再买入就不是 dp[i][0] = -prices[i]
了,
而应该是dp[i][0] = dp[i-1][1] - prices[i]
这种情况!!!
以上分析完毕,代码如下:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
//0表示持有, 1表示不持有
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i=1;i<n;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
return dp[n-1][1];
}
}
四、最佳买卖股票时机含冷冻期
第四题,看力扣第 309 题「 最佳买卖股票时机含冷冻期」,也就是 k
为正无穷,但含有交易冷冻期的情况:
相对于动态规划:122.买卖股票的最佳时机II,本题加上了一个冷冻期
在**动态规划:122.买卖股票的最佳时机II **中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。
动规五部曲,分析如下:
1、确定 dp 数组以及下标的含义
dp[i][j]
,第 i
天状态为 j
,所剩的最多现金为 dp[i][j]
。
具体可以区分出如下四个状态:
- 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
- 不持有股票状态,这里就有两种卖出股票状态
- 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
- 状态三:今天卖出股票
- 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
j
的状态为:
- 0:状态一
- 1:状态二
- 2:状态三
- 3:状态四
很多题解为什么讲的比较模糊,是因为把这四个状态合并成三个状态了,其实就是把状态二和状态四合并在一起了。
从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。
如果大家按照代码随想录顺序来刷的话,会发现 买卖股票最佳时机 1,2,3,4 的题目讲解中
- 动态规划:121.买卖股票的最佳时机(opens new window)
- 动态规划:122.买卖股票的最佳时机II(opens new window)
- 动态规划:123.买卖股票的最佳时机III(opens new window)
- 动态规划:188.买卖股票的最佳时机IV(opens new window)
「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢?
因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。
如果没有按照 代码随想录 顺序去刷的录友,可能看这里的讲解 会有点困惑,建议把代码随想录本篇之前股票内容的讲解都看一下,领会一下每天 状态的设置。
注意这里的每一个状态,例如状态一,是持有股票股票状态并不是说今天一定就买入股票,而是说保持买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态。
2、确定递推公式
达到买入股票状态(状态一)即:dp[i][0]
,有两个具体操作:
- 操作一:前一天就是持有股票状态(状态一),
dp[i][0] = dp[i - 1][0]
- 操作二:今天买入了,有两种情况
- 前一天是冷冻期(状态四),
dp[i - 1][3] - prices[i]
- 前一天是保持卖出股票的状态(状态二),
dp[i - 1][1] - prices[i]
- 前一天是冷冻期(状态四),
那么 dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
达到保持卖出股票状态(状态二)即:dp[i][1]
,有两个具体操作:
- 操作一:前一天就是状态二
- 操作二:前一天是冷冻期(状态四)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
达到今天就卖出股票状态(状态三),即:dp[i][2]
,只有一个操作:
昨天一定是持有股票状态(状态一),今天卖出
即:dp[i][2] = dp[i - 1][0] + prices[i];
达到冷冻期状态(状态四),即:dp[i][3]
,只有一个操作:
昨天卖出了股票(状态三)
dp[i][3] = dp[i - 1][2];
综上分析,递推代码如下:
dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
3、dp 数组如何初始化
这里主要讨论一下第0天如何初始化。
如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0]
,一定是当天买入股票。
保持卖出股票状态(状态二),这里其实从 「状态二」的定义来说 ,很难明确应该初始多少,这种情况我们就看递推公式需要我们给他初始成什么数值。
如果i为1,第1天买入股票,那么递归公式中需要计算 dp[i - 1][1] - prices[i]
,即 dp[0][1] - prices[1]
,那么大家感受一下 dp[0][1]
(即第0天的状态二)应该初始成多少,只能初始为0。想一想如果初始为其他数值,是我们第1天买入股票后 手里还剩的现金数量是不是就不对了。
今天卖出了股票(状态三),同上分析,dp[0][2]
初始化为0,dp[0][3]
也初始为0。
4、确定遍历顺序
从递归公式上可以看出,dp[i]
依赖于 dp[i-1]
,所以是从前向后遍历。
5、举例推导 dp 数组
以 [1,2,3,0,2] 为例,dp
数组如下:
最后结果是取 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。
代码如下:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
//4个状态:
//1.持有; 2.不持有; 3.卖出; 4.冷冻期
int[][] dp = new int[n][4];
//base case;
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
dp[0][3] = 0;
for(int i=1;i<n;i++){
dp[i][0] = Math.max(dp[i-1][0],Math.max(dp[i-1][1]-prices[i],dp[i-1][3]-prices[i]));
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][3]);
dp[i][2] = dp[i-1][0] + prices[i];
dp[i][3] = dp[i-1][2];
}
return Math.max(dp[n-1][1],Math.max(dp[n-1][2],dp[n-1][3]));
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
五、最佳买卖股票时机含冷冻期
相对于动态规划:122.买卖股票的最佳时机II,第五题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。
唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。
这里重申一下dp
数组的含义:
dp[i][0]
表示第 i
天持有股票所省最多现金。 dp[i][1]
表示第 i
天不持有股票所得最多现金
如果第 i
天持有股票即dp[i][0]
, 那么可以由两个状态推出来:
- 第
i-1
天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
- 第
i
天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格 即:dp[i - 1][1] - prices[i]
所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
再来看看如果第 i
天不持有股票即 dp[i][1]
的情况, 依然可以由两个状态推出来:
- 第
i-1
天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
- 第
i
天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了即:dp[i - 1][0] + prices[i] - fee
所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
本题和动态规划:122.买卖股票的最佳时机II 的区别就是这里需要多一个减去手续费的操作。
以上分析完毕,代码如下:
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
//0.持有; 1.不持有
int[][] dp = new int[n][2];
//base case
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i=1;i<n;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] - prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] + prices[i] - fee);
}
return dp[n-1][1];
}
}