1..买卖股票的最佳时机IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
-
示例 1:
-
输入:k = 2, prices = [2,4,1]
-
输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2。
-
示例 2:
-
输入:k = 2, prices = [3,2,6,5,0,3]
-
输出:7 解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
提示:
- 0 <= k <= 100
- 0 <= prices.length <= 1000
- 0 <= prices[i] <= 1000
思路
这道题目可以说是动态规划:123.买卖股票的最佳时机III (opens new window)的进阶版,这里要求至多有k次交易。
动规五部曲,分析如下:
- 确定dp数组以及下标的含义
在动态规划:123.买卖股票的最佳时机III (opens new window)中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组。
使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j]
j的状态表示为:
- 0 表示不操作
- 1 第一次买入
- 2 第一次卖出
- 3 第二次买入
- 4 第二次卖出
- .....
大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。
题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。
- 确定递推公式
还要强调一下: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] = max(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] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
本题和动态规划:123.买卖股票的最佳时机III (opens new window)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态。
- 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]
在初始化的地方同样要类比j为偶数是卖、奇数是买的状态。
- 确定遍历顺序
从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。
- 举例推导dp数组
以输入[1,2,3,4,5],k=2为例。
最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。
三维数组
public class Best_Time_to_Buy_and_Sell_StockIV {
public int maxProfit1(int k, int[] prices) {
if (prices.length == 0) return 0;
int len = prices.length;
int[][][] dp = new int[len][k + 1][2];//dp 是一个三维数组,其中 dp[i][j][0] 表示第 i 天结束时第 j 次交易不持有股票的最大利润,dp[i][j][1] 表示第 i 天结束时第 j 次交易持有股票的最大利润。
for (int i = 0; i <= k; i++) {
dp[0][i][1] = -prices[0];//初始化 dp[0][j][1] 为 -prices[0],表示第0天买入第j次股票。
}
for (int i = 1; i < len; i++) {//遍历价格数组,对于每个价格,更新 dp[i][j][0] 和 dp[i][j][1]:dp[i][j][0]:选择不持有股票或者在今天卖出股票,取最大值。dp[i][j][1]:选择不买入股票或者在今天买入股票,取最大值,且必须在今天之前卖出过股票。
for (int j = 1; j <= k; j++) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[len - 1][k][0];//返回最后一天不持有股票的最大利润 dp[len - 1][k][0]。
}
=
}
- 时间复杂度:O(k * len),其中len是数组
prices
的长度,k是允许的交易次数。需要遍历每一天和每一次交易。 - 空间复杂度:O(k * len),需要一个大小为len×(k+1)×2的三维数组来存储每一天、每一次交易的状态。
二维数组
public class Best_Time_to_Buy_and_Sell_StockIV {
public int maxProfit2(int k, int[] prices) {
if (prices.length == 0) return 0;
int len = prices.length;
int[][] dp = new int[len][k*2 + 1];//dp 是一个二维数组,其中 dp[i][j] 表示第 i 天结束时第 j 次交易的最大利润。
for (int i = 1; i < k*2; i += 2) {
dp[0][i] = -prices[0];//初始化 dp[0][j] 为 -prices[0],表示第0天买入第j次股票。
}
for (int i = 1; i < len; i++) {//遍历价格数组,对于每个价格,更新 dp[i][j]:dp[i][j + 1]:选择不买入股票或者在今天买入股票,取最大值。dp[i][j + 2]:选择不卖出股票或者在今天卖出股票,取最大值。
for (int j = 0; j < k*2 - 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[len - 1][k*2];//返回最后一天不持有股票的最大利润 dp[len - 1][k*2]。
}
}
- 时间复杂度:O(k * len),其中len是数组
prices
的长度,k是允许的交易次数。需要遍历每一天和每一次交易。 - 空间复杂度:O(k * len),需要一个大小为len×(k*2+1)的二维数组来存储每一天、每一次交易的状态。
一维数组
public class Best_Time_to_Buy_and_Sell_StockIV {
public int maxProfit3(int k, int[] prices) {
if(prices.length == 0){
return 0;
}
if(k == 0){
return 0;
}
int[] dp = new int[2 * k];//dp 是一个一维数组,其中 dp[j] 表示第 j 次交易的最大利润。
for(int i = 0; i < dp.length / 2; i++){
dp[i * 2] = -prices[0];//初始化 dp[0] 为 -prices[0],表示第0天买入股票。
}
for(int i = 1; i <= prices.length; i++){//遍历价格数组,对于每个价格,更新 dp:dp[j]:选择不买入股票或者在今天买入股票,取最大值。dp[j + 1]:选择不卖出股票或者在今天卖出股票,取最大值。
dp[0] = Math.max(dp[0], -prices[i - 1]);
dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]);
for(int j = 2; j < dp.length; j += 2){
dp[j] = Math.max(dp[j], dp[j - 1] - prices[i-1]);
dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i - 1]);
}
}
return dp[dp.length - 1];//返回最后一天不持有股票的最大利润 dp[dp.length - 1]。
}
}
- 时间复杂度:O(k * len),其中len是数组
prices
的长度,k是允许的交易次数。需要遍历每一天和每一次交易。 - 空间复杂度:O(k),只需要一个大小为2k的一维数组来存储状态,这是因为我们只保留了最后一次买卖的状态。
public class Best_Time_to_Buy_and_Sell_StockIV {
public int maxProfit(int k, int[] prices) {
if(prices.length == 0 || k == 0)
return 0;
int dp[] = new int [k * 2 + 1];//dp 是一个一维数组,其中 dp[j] 表示第 j 次交易的最大利润。初始化 dp[j] 为 -prices[0],表示第0天买入第j次股票。
for(int i = 1; i < 2 * k + 1; i += 2){
dp[i] = -prices[0];
}
for(int i = 1; i < prices.length; i++){//遍历价格数组,对于每个价格,更新 dp:dp[j]:选择不买入股票或者在今天买入股票,取最大值。dp[j + 1]:选择不卖出股票或者在今天卖出股票,取最大值。
for(int j = 1; j < 2 * k + 1; j++){
if(j % 2 == 1)
dp[j] = Math.max(dp[j], dp[j - 1] - prices[i]);
else
dp[j] = Math.max(dp[j], dp[j - 1] + prices[i]);
}
}
return dp[2 * k];//返回最后一天不持有股票的最大利润 dp[2 * k]。
}
}
- 时间复杂度:O(k * len),其中len是数组
prices
的长度,k是允许的交易次数。需要遍历每一天和每一次交易。 - 空间复杂度:O(k),只需要