题目描述
给你一个整数数组 prices
和一个整数 k
,其中 prices[i]
是某支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k
笔交易。也就是说,你最多可以买 k
次,卖 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 。
提示:
1 <= k <= 100
1 <= prices.length <= 1000
0 <= prices[i] <= 1000
问题分析
这道题是"买卖股票的最佳时机"系列的第四题,是前面问题的一般化:
- 最多可以完成 k 笔交易
- 每次买入前必须先卖出手中的股票
这是一个典型的动态规划问题,但比前面的题目更复杂,因为交易次数成为了一个可变参数。关键在于如何表示状态和定义状态转移方程。
解题思路
我们可以使用二维动态规划来解决这个问题。定义状态 dp[i][j] 表示在第 i 天进行了 j 次交易后的最大利润,其中 j 的取值范围是 0 到 k。
但实际上,我们需要知道当前是持有股票状态还是未持有股票状态,因此可以进一步细分为:
- dp[i][j][0]:表示第 i 天结束时,已进行 j 次交易,手上没有股票的最大利润
- dp[i][j][1]:表示第 i 天结束时,已进行 j 次交易,手上持有股票的最大利润
注意:一次完整的交易指买入然后卖出。我们认为交易次数的增加是在卖出股票时发生的。
状态转移
每天的状态会根据前一天的状态更新,分为两种情况:
- 不持有股票 (dp[i][j][0]):
- 前一天就不持有股票,今天什么都不做:dp[i-1][j][0]。
- 前一天持有股票,今天卖出,完成一次交易:dp[i-1][j-1][1] + prices[i](如果 j > 0)。
- 取两者的最大值:
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j-1][1] + prices[i])
- 持有股票 (dp[i][j][1]):
- 前一天就持有股票,今天什么都不做:dp[i-1][j][1]。
- 前一天不持有股票,今天买入:dp[i-1][j][0] - prices[i]。
- 取两者的最大值:
dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j][0] - prices[i])
状态转移方程为:
- dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
- 前一天未持有股票,今天仍未持有;或前一天持有股票,今天卖出
- dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
- 前一天持有股票,今天仍持有;或前一天未持有股票,今天买入(此时已进行的交易次数应减1)
初始化
- 第一天 (i = 0):
- dp[0][j][0] = 0:不持有股票,利润为 0。
- dp[0][j][1] = -prices[0]:持有股票,花费了第一天的股价。
- 边界情况:
- 如果 j = 0(没有交易),dp[i][0][1] 不应有正利润,通常设为负无穷(代码中会处理)。
- 如果天数少于 2 或 k = 0,直接返回 0。
最终答案
最后一天不持有股票时的最大利润,即 max(dp[n-1][0][0], dp[n-1][1][0], ..., dp[n-1][k][0])。通常 dp[n-1][k][0] 是最大值。
注意
由于在实际编码时,三维数组不好处理,我们可以用两个二维数组分别表示持有和未持有状态:
- dpSell[i][j]:表示第 i 天结束时,已进行 j 次交易,手上没有股票的最大利润
- dpBuy[i][j]:表示第 i 天结束时,已进行 j 次交易,手上持有股票的最大利润
特殊情况处理
我们的解法包含一个重要的优化:当 k >= n/2 时,问题等价于不限制交易次数的情况。因为在 n 天内,最多只能进行 n/2 次完整的交易(买入和卖出各占一天)。
当不限制交易次数时,我们可以使用一个更简单的贪心算法:只要价格上涨,就进行买卖,累计所有的正利润。
这个优化可以大大提高算法在 k 值较大时的效率,避免了不必要的计算和可能的内存溢出。
详细执行过程图解
以示例 k = 2, prices = [3,2,6,5,0,3] 为例,让我们详细跟踪算法的执行过程:
初始状态:
- 价格数组:[3,2,6,5,0,3]
- k = 2
第1天(i=0):
- 价格 = 3
- buy[1] = -3
- sell[1] = 0
- buy[2] = -3
- sell[2] = 0
第2天(i=1):
- 价格 = 2
- buy[1] = max(-3, 0-2) = -2(今天买入)
- sell[1] = max(0, -3+2) = 0(不操作)
- buy[2] = max(-3, 0-2) = -2(今天买入)
- sell[2] = max(0, -3+2) = 0(不操作)
第3天(i=2):
- 价格 = 6
- buy[1] = max(-2, 0-6) = -2(保持持有)
- sell[1] = max(0, -2+6) = 4(今天卖出)
- buy[2] = max(-2, 0-6) = -2(保持持有)
- sell[2] = max(0, -2+6) = 4(今天卖出)
第4天(i=3):
- 价格 = 5
- buy[1] = max(-2, 4-5) = -1(今天买入)
- sell[1] = max(4, -2+5) = 4(保持未持有)
- buy[2] = max(-2, 4-5) = -1(今天买入)
- sell[2] = max(4, -2+5) = 4(保持未持有)
第5天(i=4):
- 价格 = 0
- buy[1] = max(-1, 4-0) = 4(今天买入)
- sell[1] = max(4, -1+0) = 4(保持未持有)
- buy[2] = max(-1, 4-0) = 4(今天买入)
- sell[2] = max(4, -1+0) = 4(保持未持有)
第6天(i=5):
- 价格 = 3
- buy[1] = max(4, 4-3) = 4(保持持有)
- sell[1] = max(4, 4+3) = 7(今天卖出)
- buy[2] = max(4, 4-3) = 4(保持持有)
- sell[2] = max(4, 4+3) = 7(今天卖出)
最终结果 = sell[2] = 7
这个结果对应的交易是:
- 在第2天(价格=2)买入,在第3天(价格=6)卖出,利润=4
- 在第5天(价格=0)买入,在第6天(价格=3)卖出,利润=3
总利润 = 4 + 3 = 7
价格
^
6 | *
|
5 | *
|
4 |
|
3 |* *
|
2 | *
|
1 |
|
0 | *
+---------------> 时间
1 2 3 4 5 6
交易策略:
第2天: 买入 (价格=2)
第3天: 卖出 (价格=6, 利润=4)
第5天: 买入 (价格=0)
第6天: 卖出 (价格=3, 利润=3)
总利润: 4+3=7
动态规划解法
Java 实现
class Solution {
public int maxProfit(int k, int[] prices) {
// 边界条件检查
if (prices == null || prices.length <= 1 || k <= 0) {
return 0;
}
int n = prices.length;
// 特殊情况:当 k 大于 n/2 时,问题等价于不限制交易次数
if (k >= n / 2) {
return maxProfitUnlimited(prices);
}
// dpBuy[i][j]:第i天结束时,已进行j次交易,手上有股票的最大利润
// dpSell[i][j]:第i天结束时,已进行j次交易,手上没有股票的最大利润
int[][] dpBuy = new int[n][k + 1];
int[][] dpSell = new int[n][k + 1];
// 初始化第一天的状态
for (int j = 0; j <= k; j++) {
dpBuy[0][j] = -prices[0]; // 第一天买入股票
dpSell[0][j] = 0; // 第一天不操作
}
// 动态规划过程
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
// 第i天可以选择买入或不操作
dpBuy[i][j] = Math.max(dpBuy[i-1][j], dpSell[i-1][j-1] - prices[i]);
// 第i天可以选择卖出或不操作
dpSell[i][j] = Math.max(dpSell[i-1][j], dpBuy[i-1][j] + prices[i]);
}
}
// 返回最后一天进行k次交易,手上没有股票的最大利润
return dpSell[n-1][k];
}
// 处理不限制交易次数的情况
private int maxProfitUnlimited(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
C# 实现
public class Solution {
public int MaxProfit(int k, int[] prices) {
// 边界条件检查
if (prices == null || prices.Length <= 1 || k <= 0) {
return 0;
}
int n = prices.Length;
// 特殊情况:当 k 大于 n/2 时,问题等价于不限制交易次数
if (k >= n / 2) {
return MaxProfitUnlimited(prices);
}
// dpBuy[i][j]:第i天结束时,已进行j次交易,手上有股票的最大利润
// dpSell[i][j]:第i天结束时,已进行j次交易,手上没有股票的最大利润
int[][] dpBuy = new int[n][];
int[][] dpSell = new int[n][];
for (int i = 0; i < n; i++) {
dpBuy[i] = new int[k + 1];
dpSell[i] = new int[k + 1];
}
// 初始化第一天的状态
for (int j = 0; j <= k; j++) {
dpBuy[0][j] = -prices[0]; // 第一天买入股票
dpSell[0][j] = 0; // 第一天不操作
}
// 动态规划过程
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
// 第i天可以选择买入或不操作
dpBuy[i][j] = Math.Max(dpBuy[i-1][j], dpSell[i-1][j-1] - prices[i]);
// 第i天可以选择卖出或不操作
dpSell[i][j] = Math.Max(dpSell[i-1][j], dpBuy[i-1][j] + prices[i]);
}
}
// 返回最后一天进行k次交易,手上没有股票的最大利润
return dpSell[n-1][k];
}
// 处理不限制交易次数的情况
private int MaxProfitUnlimited(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.Length; i++) {
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
空间优化解法
观察标准动态规划解法中的状态转移方程,我们可以发现,对于第 i 天的状态,我们只需要第 i-1 天的状态,所以可以将空间复杂度从 O(nk) 降低到 O(k)。
Java 空间优化实现
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length <= 1 || k <= 0) {
return 0;
}
int n = prices.length;
// 特殊情况:当 k 大于 n/2 时,问题等价于不限制交易次数
if (k >= n / 2) {
return maxProfitUnlimited(prices);
}
// 定义两个数组来分别记录当前持有股票和未持有股票的最大利润
int[] buy = new int[k + 1]; // buy[j]: 进行j次交易时,持有股票的最大利润
int[] sell = new int[k + 1]; // sell[j]: 进行j次交易时,未持有股票的最大利润
// 初始化
Arrays.fill(buy, -prices[0]);
Arrays.fill(sell, 0);
// 动态规划过程
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
// 计算第i天的状态
buy[j] = Math.max(buy[j], sell[j-1] - prices[i]);
sell[j] = Math.max(sell[j], buy[j] + prices[i]);
}
}
return sell[k];
}
private int maxProfitUnlimited(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
C# 空间优化实现
public class Solution {
public int MaxProfit(int k, int[] prices) {
if (prices == null || prices.Length <= 1 || k <= 0) {
return 0;
}
int n = prices.Length;
// 特殊情况:当 k 大于 n/2 时,问题等价于不限制交易次数
if (k >= n / 2) {
return MaxProfitUnlimited(prices);
}
// 定义两个数组来分别记录当前持有股票和未持有股票的最大利润
int[] buy = new int[k + 1]; // buy[j]: 进行j次交易时,持有股票的最大利润
int[] sell = new int[k + 1]; // sell[j]: 进行j次交易时,未持有股票的最大利润
// 初始化
for (int j = 0; j <= k; j++) {
buy[j] = -prices[0];
sell[j] = 0;
}
// 动态规划过程
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
// 计算第i天的状态
buy[j] = Math.Max(buy[j], sell[j-1] - prices[i]);
sell[j] = Math.Max(sell[j], buy[j] + prices[i]);
}
}
return sell[k];
}
private int MaxProfitUnlimited(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.Length; i++) {
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
复杂度分析
- 时间复杂度:
- 常规情况:O(nk),其中 n 是天数,k 是交易次数限制
- 特殊情况(k >= n/2):O(n)
- 空间复杂度:
- 标准DP版本:O(nk)
- 空间优化版本:O(k)