力扣123. 买卖股票的最佳时机 III

0 阅读6分钟

引言

今天,我们将深入探讨一个经典的算法问题:最多完成两笔交易的股票买卖问题。这个问题不仅考验我们对动态规划的理解,更体现了分治思想在算法设计中的巧妙应用。

问题描述

给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格。你最多可以完成两笔交易(即买入和卖出各两次),但必须遵守以下规则:

  1. 不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)
  2. 卖出股票后,无法在第二天买入股票(有1天的冷却期)

我们的目标是设计一个算法,计算能获得的最大利润。

暴力解法与优化思路

暴力法的局限性

最直观的解法是枚举所有可能的买卖组合。对于n天的价格数据,我们需要考虑:

  • 第一次买入时间:n种选择
  • 第一次卖出时间:最多n种选择
  • 第二次买入时间:最多n种选择
  • 第二次卖出时间:最多n种选择

这导致时间复杂度高达O(n⁴),显然不可行。

关键洞察:交易拆分

观察问题的本质,我们可以发现一个重要特性:两次交易是独立的。虽然有时间顺序的约束,但我们可以将整个时间段划分为两个独立的区间:

  • 第一次交易发生在区间 [0, i]
  • 第二次交易发生在区间 [i, n-1]

这样,问题就转化为:寻找一个分割点i,使得左区间的最大利润加上右区间的最大利润最大化。

算法详解:分治预处理法

核心思想

我们通过预处理计算两个数组:

  • left[i]:表示在第i天或之前完成一次交易能获得的最大利润
  • right[i]:表示在第i天或之后完成一次交易能获得的最大利润

然后遍历所有可能的分割点i,计算 left[i] + right[i] 的最大值。

算法步骤

1. 计算左数组 left

int[] left = new int[n];
int minPrice = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
    minPrice = Math.min(minPrice, prices[i]);
    if (i > 0) {
        left[i] = Math.max(left[i - 1], prices[i] - minPrice);
    }
}

逻辑解释

  • minPrice 记录到第i天为止的最低价格

  • left[i] 有两种选择:

    • 不在第i天卖出:利润等于 left[i-1]
    • 在第i天卖出:利润为 prices[i] - minPrice
  • 取两者中的较大值

2. 计算右数组 right

int[] right = new int[n];
int maxPrice = Integer.MIN_VALUE;
for (int i = n - 1; i >= 0; i--) {
    maxPrice = Math.max(maxPrice, prices[i]);
    if (i < n - 1) {
        right[i] = Math.max(right[i + 1], maxPrice - prices[i]);
    }
}

逻辑解释

  • maxPrice 记录从第i天开始的最高价格

  • right[i] 有两种选择:

    • 不在第i天买入:利润等于 right[i+1]
    • 在第i天买入:利润为 maxPrice - prices[i]
  • 取两者中的较大值

3. 合并结果

int maxProfit = 0;
for (int i = 0; i < n; i++) {
    maxProfit = Math.max(maxProfit, left[i] + right[i]);
}
return maxProfit;

完整代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n < 2) return 0;
        
        // 左数组:记录到第i天为止的最大利润
        int[] left = new int[n];
        int minPrice = prices[0];
        for (int i = 1; i < n; i++) {
            minPrice = Math.min(minPrice, prices[i]);
            left[i] = Math.max(left[i - 1], prices[i] - minPrice);
        }
        
        // 右数组:记录从第i天开始的最大利润
        int[] right = new int[n];
        int maxPrice = prices[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            maxPrice = Math.max(maxPrice, prices[i]);
            right[i] = Math.max(right[i + 1], maxPrice - prices[i]);
        }
        
        // 合并结果
        int maxProfit = 0;
        for (int i = 0; i < n; i++) {
            maxProfit = Math.max(maxProfit, left[i] + right[i]);
        }
        
        return maxProfit;
    }
}

复杂度分析

时间复杂度:O(n)

  • 计算左数组:一次遍历,O(n)
  • 计算右数组:一次遍历,O(n)
  • 合并结果:一次遍历,O(n)
  • 总时间复杂度:O(3n) = O(n)

空间复杂度:O(n)

  • 需要两个辅助数组 leftright,每个大小为n
  • 总空间复杂度:O(2n) = O(n)

示例演示

让我们通过一个具体例子来理解算法的执行过程:

输入prices = [3, 3, 5, 0, 0, 3, 1, 4]

步骤1:计算左数组

天数:  0   1   2   3   4   5   6   7
价格:  3   3   5   0   0   3   1   4
left:  0   0   2   2   2   3   3   4

步骤2:计算右数组

天数:  0   1   2   3   4   5   6   7
价格:  3   3   5   0   0   3   1   4
right: 4   4   4   4   4   3   3   0

步骤3:合并结果

i=0: left[0]+right[0] = 0+4 = 4
i=1: 0+4 = 4
i=2: 2+4 = 6  ← 最大值
i=3: 2+4 = 6
i=4: 2+4 = 6
i=5: 3+3 = 6
i=6: 3+3 = 6
i=7: 4+0 = 4

最大利润:6

交易策略

  • 第一次交易:第3天买入(0),第5天卖出(3),利润3
  • 第二次交易:第6天买入(1),第7天卖出(4),利润3
  • 总利润:3+3=6

算法优化:空间复杂度优化

虽然上述算法的空间复杂度为O(n),但我们还可以进一步优化到O(1):

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n < 2) return 0;
        
        // 第一次交易的状态
        int buy1 = Integer.MAX_VALUE;
        int profit1 = 0;
        
        // 第二次交易的状态
        int buy2 = Integer.MAX_VALUE;
        int profit2 = 0;
        
        for (int price : prices) {
            // 更新第一次交易
            buy1 = Math.min(buy1, price);
            profit1 = Math.max(profit1, price - buy1);
            
            // 更新第二次交易(考虑第一次交易的利润)
            buy2 = Math.min(buy2, price - profit1);
            profit2 = Math.max(profit2, price - buy2);
        }
        
        return profit2;
    }
}

优化原理

  • buy1:第一次买入的最低成本
  • profit1:第一次交易的最大利润
  • buy2:第二次买入的实际成本(已扣除第一次利润)
  • profit2:两次交易的总最大利润

这种方法将空间复杂度降低到O(1),同时保持了O(n)的时间复杂度。

扩展思考

1. 最多k次交易

对于最多k次交易的情况,我们可以使用动态规划:

dp[i][j] = max(dp[i][j-1], prices[j] + maxDiff)
其中 maxDiff = max(maxDiff, dp[i-1][j] - prices[j])

2. 包含冷却期

如果卖出后需要等待一天才能再次买入,状态转移方程需要调整:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])

3. 包含交易手续费

每次交易需要支付固定手续费fee:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)

总结

通过本文的分析,我们深入探讨了最多完成两笔交易的股票买卖问题。关键要点包括:

  1. 分治思想:将复杂问题拆分为两个独立的子问题
  2. 预处理技巧:通过计算左右数组避免重复计算
  3. 空间优化:进一步将空间复杂度从O(n)优化到O(1)
  4. 扩展性:算法思想可以扩展到更多交易次数和更复杂的约束条件