股票问题
股票问题最核心的问题是根据条件来判断何时买入和卖出股票,以此来获得利润的最大值,如{1, 3, 2, 8, 4, 9},第一天买入最后一天卖出所获利润(只能买卖一次)为:9-1=8,而每一天的状态可分为四类:当天买入股票、当天卖出股票、已买入股票当天不买也不卖、已卖出股票不买也不卖。因此可以定义一个dp数组:dp[i][j],其中i表示天数,j为状态,易知i的取值范围为0~i-1(闭区间),j的取值范围为0~3(对应四种状态的每一个)。通过分析可以发现,当天买入股票和已买入股票当天不买也不卖这两个状态可以合并为当天持有股票的状态,其中包括当天买入和已买入的情况。同理当天卖出股票和已卖出股票不买也不卖这两个状态也可以合并为当天不持有股票的状态,其中包括当天卖出股票和已卖出股票的情况,至于某一天到底是选择当天买入卖出还是保持之前的状态,这取决于哪个方式所能获得利润最大,这样便可将状态从4种优化成2种。当然对于不同的股票问题状态有不同的定义方法,但是都是以持有还是不持有股票为基础,在此基础上进行延申和拓展。
#买卖股票的最佳时机
动态规划:121.买卖股票的最佳时机,股票只能买卖一次,问最大利润。如{1, 3, 2, 8, 4, 9},第一天买入最后一天卖出所获利润(只能买卖一次)为:9-1=8。
【暴力解法】
由于只能一次买入和一次卖出,因此可以计算出所有情况下买入卖出的最大利润,取出最大值即可。
public int maxProfit2(int[] prices) {
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
for (int j = i + 1; j < prices.length; j++) {
//记录当前利润最大值
maxProfit = Math.max(maxProfit, (prices[j] - prices[i]));
}
}
return maxProfit;
}
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
【动态规划】
- dp[i][0] 表示第i天持有股票所获得的利润。
- dp[i][1] 表示第i天不持有股票所获得的利润。
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
- 第i-1天就持有股票,那么就保持现状,所获得的利润就是昨天持有股票的所获得的利润 即:dp[i - 1][0]
- 第i天买入股票,所获得的利润就是买入今天的股票后所获得的利润即:-prices[i] 所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
- 由于只能买卖一次股票,故当天买入股票的利润一定是第一次买股票的情况,之前均未买卖过股票,所获利润为0,则当天买入股票所获利润为0-prices[i]。
如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来
- 第i-1天就不持有股票,那么就保持现状,所获得的利润就是昨天不持有股票所获得的利润 即:dp[i - 1][1]
- 第i天卖出股票,所获得的利润就是按照今天股票佳价格卖出后所获得的利润即:prices[i] + dp[i - 1][0] 所以dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
public int maxProfit(int[] prices) {
//初始化dp数组
int[][] dp = new int[prices.length][2];
//dp[0][0]即第0天持有股票的利润,之前无法持有股票,故只能购入股票
dp[0][0] = -prices[0];
//dp[0][1]即第0天不持有股票的利润,而之前没有购入过股票,故为0
dp[0][1] = 0;
//遍历dp数组,从前往后遍历
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
//打印dp数组,验证是否满足预期
Arrays.stream(dp).forEach(array -> System.out.println(Arrays.toString(array)));
//dp[prices.length - 1][1]即最后一天不持有股票所获的最大利润
return dp[prices.length - 1][1];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n*2)
#买卖股票的最佳时机II
动态规划:122.买卖股票的最佳时机II可以多次买卖股票,问最大收益。如{7,1,5,3,6,4},在第 1 天(从0开始计算)(股票价格=1) 的时候买入,第 2 天(股票价格=5) 的时候卖出,在第 3 天(股票价格=3)的时候买入,在第 4 天(股票价 =6)的时候卖出,所获利润为4 + 3 = 7.
【贪心解法】
由于股票可以在同一天买入,同一天卖出,没有次数限制,将全局最大利润拆分成局部最大利润,收割每一天的正利润累加,即买卖多次获得利益。
public int maxProfit(int[] prices) {
int balance = 0;
for (int i = 1; i < prices.length; i++) {
//收割每一天的正利润累加,即买卖多次获得利益
balance += Math.max(prices[i] - prices[i - 1], 0);
}
return balance;
}
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
【动态规划】
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]
注意这里和121. 买卖股票的最佳时机唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况。在上一题中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。故当天买入股票所获的最大利润为:dp[i-1][1] - prices[i],其中dp[i-1][1]是前一天不持有股票时所获的利润(因为可多次买卖,在此次买股票前之前可能进行进行过买卖,有利益了已经)。
public int maxProfit2(int[] prices) {
//初始化dp数组
int[][] dp = new int[prices.length][2];
//dp[0][0]即第0天持有股票的利润,之前无法持有股票,故只能购入股票
dp[0][0] = -prices[0];
//dp[0][1]即第0天不持有股票的利润,而之前没有购入过股票,故为0
dp[0][1] = 0;
//遍历dp数组,从前往后遍历
for (int i = 1; i < prices.length; i++) {
//dp[i - 1][1] - prices[i]表示第i-1天不持有股票时所获的利润再减去第i天购买股票的钱,就是第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]);
}
//打印dp数组,验证是否满足预期
Arrays.stream(dp).forEach(array -> System.out.println(Arrays.toString(array)));
//dp[prices.length - 1][1]即最后一天不持有股票所获的最大利润
return dp[prices.length - 1][1];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n*2)
#买卖股票的最佳时机III
动态规划:123.买卖股票的最佳时机III最多买卖两次,问最大收益。如{3,3,5,0,0,3,1,4},在第 3 天(从0开始计算)(股票价格=0)的时候买入,在第 5 天(股票价格 = 3)的时候卖出,在第 6 天(股票价格 = 1)的时候买入,在第 7 天 (股票价格 = 4)的时候卖出,所能获得的最大利润为(3-0)+(4-1)=6。
【动态规划】
本题与前两题的不同之处在于,第一题是只能买卖一次,状态可以分为持有股票和不持有股票(默认为第一次),而第二题虽然是可以无限次买卖,但是每次买卖之间互不影响,可将多次买卖拆分成多个单次买卖的情况,只不过要在买入之前考虑之前已经通过买卖所获的利润。而本题是最多买卖两次,可以买卖一次,也可以买卖两次,因此需要记录每次持有股票时是第几次持有股票,不持有股票时是第几次不持有股票,故需要四个状态来记录:0-第一次持有股票,1-第一次不持有股票,2-第二次持有股票,3-第二次不持有股票。至于为什么最大的利润是第二次不持有股票时所获得的利润,这是因为第二次不持有股票时所获利润是根据前面第二次持有股票和第一次不持有股票时所获的利润之间通过比较取最大值得来的,覆盖了之前的情况,故最大利润为dp[prices.length - 1][3]。
dp[i][j]中 i表示第i天,j为 [0 - 3] 四个状态,dp[i][j]表示第i天状态j所获得的利润。
达到dp[i][0]状态,有两个具体操作:
- 操作一:第i天买入股票了,那么所获的利润为前一天不持有股票时的利润-当天买股票所花的费用:dp[i][0] = dp[i-1][1] - prices[i]
- 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][0] = dp[i - 1][0]
dp[i][0] = max(dp[i-1][0] - 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] = max(dp[i - 1][0] + prices[i], dp[i - 1][1])
同理可推出剩下状态部分:
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
public int maxProfit(int[] prices) {
//dp数组初始化
int[][] dp = new int[prices.length][4];
//dp[0][0]表示第0天第一次持有股票,即购买股票:dp[0][0] = -prices[0]
dp[0][0] = -prices[0];
//dp[0][1]表示第0天第一次不持有股票,而第一天不持有股票就是不购买股票:dp[0][1] = 0
dp[0][1] = 0;
//dp[0][2]表示第0天第二次持有股票,第二次持有股票是通过第一次买入和卖出股票后来得到的,相当于第0天买入股票后立即卖出,然后再买入一次股票,则dp[0][2] = -prices[0]
dp[0][2] = -prices[0];
//dp[0][3]表示第0天第二次不持有股票,相当于第一次买股票后卖出,然后第二次买股票后再卖出,此时的最大利润肯定为0,故dp[0][3] = 0
dp[0][3] = 0;
//从前往后遍历dp数组
for (int i = 1; i < prices.length; i++) {
//第一次持有股票的最大利润
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
//第一次不持有股票的最大利润
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
//第二次持有股票的最大利润
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
//第二次不持有股票的最大利润
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
}
//打印dp数组,验证是否符合预期
Arrays.stream(dp).forEach(array -> System.out.println(Arrays.toString(array)));
//dp[prices.length-1][3]即最后一天第二次不持有股票所获的最大利润,由于第二次不持有股票的最大利润覆盖了第二次持有股票的最大利润的情况,故返回第二次不持有股票的最大利润
return dp[prices.length - 1][3];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n*4)
#买卖股票的最佳时机IV
动态规划:188.买卖股票的最佳时机IV最多买卖k笔交易,问最大收益。如k = 2, prices = [3,2,6,5,0,3],即最多买卖2次。
通过分析本题可知,此题和上一题#买卖股票的最佳时机III相似,只不过上一题是最多买卖2次,而本题至多买卖k次。对于至多买卖两次的情况下,会存在第一次持有和不持有、第二次持有和不持有共四种情况,那么对于至多买卖k次的情况下,会存在第一次持有和不持有、第二次持有和不持有、...... 、第k次持有和不持有的情况,故一共有0~2*k(闭区间)个状态。而将状态具体展开:0-表示第一次持有股票,1-表示第一次不持有股票,2-表示第二次持有股票,3-表示第二次不持有股票,4-表示第三次持有股票,5-表示第三次不持有股票,..... ,通过分析可以看出为偶数时表示持有股票的状态,为奇数时表示不持有股票的状态。因此而持有股票的操作均为max{之前就持有,之前不持有-今天买入股票},而不持有股票的操作均为max{之前就不持有,之前持有+今天卖出股票},故可根据奇偶情况来进行划分处理。
public int maxProfit(int k, int[] prices) {
//定义dp数组
int[][] dp = new int[prices.length][2 * k];//2 * k用于记录0~2*k-1个状态
//初始化dp数组
for (int i = 0; i < 2 * k; i++) {
if (i % 2 == 0)
dp[0][i] = -prices[0];
}
//遍历dp数组
for (int i = 1; i < prices.length; i++) {
for (int j = 0; j < 2 * k; j++) {
//状态为偶数,说明是持有股票的状态
if (j % 2 == 0) {
//当j为0时表示第一次持有股票的情况,这种情况下只能购买当前股票
if (j == 0)
dp[i][j] = Math.max(dp[i - 1][j], -prices[i]);
//当j不为0说明是第j次持有股票(非第一次),则其最大利润为只有不持有股票的最大利润-买入当前股票的价格
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
}
//状态为奇数,说明是不持有股票的状态
else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);
}
}
}
//打印dp数组,验证是否满足预期
Arrays.stream(dp).forEach(array -> System.out.println(Arrays.toString(array)));
//dp[prices.length - 1][2 * k - 1]即最后一天第k次不持有股票所获的最大利润,由于第k次不持有股票的最大利润覆盖了第k次持有股票的最大利润的情况,故返回第k次不持有股票的最大利润
return dp[prices.length - 1][2 * k - 1];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n*2k)
#最佳买卖股票时机含冷冻期
动态规划:309.最佳买卖股票时机含冷冻期可以多次买卖但每次卖出有冷冻期1天。如{1,2,3,0,2},对应交易状态为:[买入, 卖出, 冷冻期, 买入, 卖出]。
相对于动态规划:122.买卖股票的最佳时机II,本题加上了一个冷冻期。常规做法为根据冷冻期设置四个状态进行求解,但个人认为较为复杂,具体解法可根据链接进行查看。主要分析另一种简单的方法,通过分析可以发现本题最主要的区别在于卖出股票后会进入冷冻期,无法买入,那么在冷冻期间其所已经所获的利润是不变的,故只需在下次买入的时候根据前天的状态买入即可,则还是设置两个状态分别表示持有和不持有股票的状态,0-持有股票,1-不持有股票,那么dp[i][0] = max{dp[i-1][0],dp[i-2][1]-prices[i]},dp[i][1] = {dp[i-1][1],dp[i-1][0] + prices[i]},通过分析上面的状态转移方程可以发现,不同点在于当天买入股票时为:dp[i-2][1]-prices[i],对于今天持有,昨天不持有,前天不持有时即[卖出,冷冻期,买入]的情况一定成立,而对于以前持有的状态,那么昨天肯定也持有,故也成立,对于今天才持有,昨天不持有,前天持有(×不可能有这种情况,没有冷冻),故可以用dp[i-2][1]-prices[i]来表示当天买入股票的状态。
public int maxProfit(int[] prices) {
if (prices.length == 1)
return 0;
//初始化dp数组
int[][] dp = new int[prices.length][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[1][0] = Math.max(-prices[0], -prices[1]);
dp[1][1] = Math.max(0, -prices[0] + prices[1]);
//遍历dp数组,从前往后遍历
for (int i = 2; i < prices.length; i++) {
//今天才持有,昨天不持有,前天不持有(√)
//今天才持有,昨天不持有,前天持有(×不可能有这种情况,没有冷冻)
//以前持有,昨天肯定持有(√)
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i]);
//今天才不持有,昨天持有(√)
//以前就不持有,昨天一定不持有(√)
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
//dp[prices.length-1][1]即最后一天不持有股票所获的最大利润,不持有股票的最大利润覆盖了持有股票的最大利润的情况,故返回不持有股票的最大利润
return dp[prices.length - 1][1];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n*2)
#买卖股票的最佳时机含手续费
动态规划:714.买卖股票的最佳时机含手续费可以多次买卖,但每次有手续费。如:{1, 3, 2, 8, 4, 9},free=2,总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8。
相对于动态规划:122.买卖股票的最佳时机II,本题也属于可无限次买卖的情况,但不同的时每次卖出股票的时候需要额外的手续费,则只需要在计算卖出操作的时候减去手续费就可以了。其余代码和分析相同。
public int maxProfit(int[] prices, int fee) {
//定义并初始化dp数组
int[][] dp = new int[prices.length][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
//从前往后遍历dp数组
for (int i = 1; i < prices.length; 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);//dp[i - 1][0] + prices[i] - fee是指卖出股票的情况,还需要支付额外的手续费即减去free
}
//打印dp数组,验证是否满足预期
Arrays.stream(dp).forEach(array -> System.out.println(Arrays.toString(array)));
//dp[prices.length-1][1]即最后一天不持有股票所获的最大利润,不持有股票的最大利润覆盖了持有股票的最大利润的情况,故返回不持有股票的最大利润
return dp[prices.length - 1][1];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n*2)
写在最后:本文是个人在刷完代码随想录对应的股票问题后结合其总结加上的一些个人理解汇总而成,若有歧义无法理解或错误的地方可在其总结的文章中进行查看:代码随想录 (programmercarl.com)