一、题目描述
给你一个整数数组 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
二、解题思路
这个问题可以通过动态规划来解决。我们可以定义一个三维数组 dp[i][j][l]
,其中 i
表示天数,j
表示交易次数,l
表示是否持有股票(0 表示不持有,1 表示持有)。dp[i][j][l]
的值表示在第 i
天,已经完成了 j
笔交易,并且当前是否持有股票的状态下,能够获得的最大利润。
解题思路如下:
-
初始化
dp
数组。dp[0][j][0]
应该初始化为 0,因为第一天不持有股票的利润为 0;dp[0][j][1]
应该初始化为-prices[0]
,因为第一天买入股票的利润为负的股票价格。 -
对于每一天
i
,我们可以选择不进行交易,那么dp[i][j][0]
应该是前一天不持有股票或者前一天持有股票并在今天卖出后的最大值;dp[i][j][1]
应该是前一天持有股票或者前一天不持有股票并在今天买入后的最大值。 -
我们需要对每一天和每一次交易进行遍历,并更新
dp
数组。 -
最终答案为
dp[n-1][k][0]
,因为最后一天卖出股票的利润肯定比持有股票的利润高。
三、具体代码
public class Solution {
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) return 0;
int n = prices.length;
// 如果交易次数 k 大于天数的一半,那么交易次数可以视为无限
if (k > n / 2) {
return maxProfitWithUnlimitedTransactions(prices);
}
int[][][] dp = new int[n][k + 1][2];
// 初始化 dp 数组
for (int j = 0; j <= k; j++) {
dp[0][j][0] = 0;
dp[0][j][1] = -prices[0];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
// 第 i 天不持有股票的最大利润
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
// 第 i 天持有股票的最大利润
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[n - 1][k][0];
}
// 当交易次数无限时,计算最大利润
private int maxProfitWithUnlimitedTransactions(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;
}
}
在这个代码中,我们首先检查了 k
是否大于天数的一半,如果是的话,我们可以把问题简化为无限次交易的情况,这时候我们只需要遍历价格数组,累加所有上升段的价格差即可。如果 k
不大,我们就使用三维 dp
数组来计算最大利润。
四、时间复杂度和空间复杂度
1. 时间复杂度
- 首先,我们有一个初始化
dp
数组的循环,它遍历了k
次,这是一个 O(k) 的操作。 - 接着,我们有两个嵌套循环,外层循环遍历天数
i
,这是一个 O(n) 的操作,内层循环遍历交易次数j
,这是一个 O(k) 的操作。因此,这两个嵌套循环构成了 O(n*k) 的时间复杂度。 - 当
k > n/2
时,我们调用了maxProfitWithUnlimitedTransactions
方法,该方法遍历价格数组,这是一个 O(n) 的操作。
综合上述,当 k <= n/2
时,时间复杂度为 O(nk),当 k > n/2
时,时间复杂度为 O(n)。然而,通常我们会考虑最坏情况,因此总体时间复杂度可以认为是 O(nk)。
2. 空间复杂度
- 我们定义了一个三维数组
dp
,其大小为n x (k + 1) x 2
,因此空间复杂度为 O(nk2)。 - 在
maxProfitWithUnlimitedTransactions
方法中,我们只使用了常数空间,因此该部分的空间复杂度为 O(1)。
综上所述,空间复杂度为 O(nk2),通常我们可以简化表示为 O(n*k),因为常数因子不影响渐近空间复杂度。
五、总结知识点
-
动态规划 (Dynamic Programming, DP):
- 动态规划是一种算法设计技术,用于求解具有重叠子问题和最优子结构的问题。
- 在此代码中,使用三维数组
dp
来存储中间状态,从而避免重复计算。
-
嵌套循环:
- 代码中使用了嵌套循环来遍历天数和交易次数,以更新
dp
数组中的状态。
- 代码中使用了嵌套循环来遍历天数和交易次数,以更新
-
状态转移方程:
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i])
表示在第i
天,不持有股票的最大利润。dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i])
表示在第i
天,持有股票的最大利润。
-
贪心算法 (Greedy Algorithm):
- 当交易次数
k
大于天数的一半时,问题可以简化为无限次交易,此时可以使用贪心算法,通过累加所有上升段的价格差来获得最大利润。
- 当交易次数
-
数组索引和边界条件:
- 代码中处理了数组索引,确保不会越界。例如,在初始化
dp
数组时,使用了j <= k
来确保只初始化到k
次。
- 代码中处理了数组索引,确保不会越界。例如,在初始化
-
方法的封装:
- 将无限次交易情况下的最大利润计算封装为
maxProfitWithUnlimitedTransactions
方法,提高了代码的可读性和可维护性。
- 将无限次交易情况下的最大利润计算封装为
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。