LeetCode 123. 买卖股票的最佳时机 III & 188. 买卖股票的最佳时机 IV

16 阅读10分钟

股票买卖系列是LeetCode动态规划模块的经典题型,其中123题(最多2笔交易)和188题(最多k笔交易)一脉相承,前者是后者的特殊情况(k=2),掌握两者的解题逻辑,能轻松吃透“有限次交易”类股票问题。本文将结合官方最优解法,详细拆解两道题的思路、代码细节,以及两者的关联与区别,帮你快速掌握这类题的核心套路。

一、LeetCode 123. 买卖股票的最佳时机 III(最多2笔交易)

1. 题目核心要求

给定一个股票价格数组prices,第i个元素是第i天的股票价格,要求设计算法计算最大利润,最多可完成2笔交易,且不能同时持有多支股票(再次购买前必须出售之前的股票)。

2. 解题思路(动态规划)

这类有限次交易的股票问题,核心是用动态规划跟踪“不同交易次数下的持有/不持有股票状态”,因为最多2笔交易,状态数量有限,无需用二维数组,直接用变量即可优化空间复杂度。

关键状态定义(每天更新4个核心变量):

  • buy1:完成0笔交易后,持有1支股票的最大利润(本质是“第一次买入”的最优状态,此时利润为负,即 -prices[i]);

  • sell1:完成1笔交易后,不持有股票的最大利润(第一次卖出后的最优收益);

  • buy2:完成1笔交易后,持有1支股票的最大利润(第二次买入的最优状态,用第一次卖出的利润减去当前股价);

  • sell2:完成2笔交易后,不持有股票的最大利润(第二次卖出后的最优收益,也是我们最终要返回的结果)。

状态转移逻辑:每天遍历股价,依次更新4个变量,保证每个状态都是当前最优(取最大值)。因为交易有先后顺序(必须先买后卖、先完成第一笔再完成第二笔),所以更新顺序不能乱,必须先更新buy1、sell1,再更新buy2、sell2。

3. 代码解析(最优解法,时间O(n),空间O(1))

function maxProfit(prices: number[]): number {
  const len = prices.length;
  // 初始化:第一天买入两次(理论上不可能,但初始值不影响后续更新,统一设为-prices[0])
  let buy1 = -prices[0], buy2 = -prices[0];
  // 初始化:未进行任何交易时,卖出利润为0
  let sell1 = 0, sell2 = 0;
  for (let i = 0; i < len; i++) {
    // 第一次买入:取“之前的buy1”和“当前股价买入(-prices[i])”的最大值
    buy1 = Math.max(buy1, -prices[i]);
    // 第一次卖出:取“之前的sell1”和“当前buy1 + 股价(卖出获利)”的最大值
    sell1 = Math.max(sell1, buy1 + prices[i]);
    // 第二次买入:取“之前的buy2”和“第一次卖出的利润 - 当前股价(用第一笔利润买第二笔)”的最大值
    buy2 = Math.max(buy2, sell1 - prices[i]);
    // 第二次卖出:取“之前的sell2”和“当前buy2 + 股价(第二笔卖出获利)”的最大值
    sell2 = Math.max(sell2, buy2 + prices[i]);
  }
  // 最多完成2笔交易,sell2就是完成2笔的最大利润;若只完成1笔,sell2也会等于sell1(第二笔未进行)
  return sell2;
};

关键细节补充:

  • 初始化时,buy1和buy2都设为 -prices[0],因为第一天如果买入,无论第一次还是第二次(虽然实际不能同时买,但初始值不影响,后续会被更新),利润都是 -prices[0];

  • 循环从i=0开始(也可从i=1开始,结果一致),因为第一天的股价会更新buy1,后续天数逐步优化各个状态;

  • 最终返回sell2,因为sell2包含了“完成0笔、1笔、2笔交易”的最优情况——若0笔交易,sell2=0;若1笔交易,sell2=sell1;若2笔交易,sell2就是最优值。

4. 示例验证

示例1:prices = [3,3,5,0,0,3,1,4] → 最大利润6

解析:第一次在第4天(价格0)买入,第6天(价格3)卖出,获利3;第二次在第7天(价格1)买入,第8天(价格4)卖出,获利3,总利润6,对应sell2的最终值。

示例2:prices = [1,2,3,4,5] → 最大利润4

解析:最优策略是只做1笔交易(第1天买,第5天卖),此时sell2 = sell1 = 4,符合预期。

二、LeetCode 188. 买卖股票的最佳时机 IV(最多k笔交易)

1. 题目核心要求

给定股票价格数组prices和整数k,要求计算最多完成k笔交易的最大利润,规则同上(不能同时持有多支股票,先卖后买)。

注意:这道题是123题的通用版,当k=2时,解法完全可以复用123题的思路;当k≥len/2时(len是prices长度),相当于“无限次交易”(因为每天最多完成1笔交易,len天最多完成len/2笔),此时可简化为“买卖股票最佳时机II”的解法(贪心),但本文重点讲解通用动态规划解法。

2. 解题思路(动态规划,通用版)

当交易次数扩展到k次时,用变量无法满足状态跟踪需求,因此需要用两个数组分别记录“第j笔交易的持有状态”和“第j笔交易的卖出状态”:

  • buy[j]:完成j-1笔交易后,持有1支股票的最大利润(即第j次买入后的最优状态);

  • sell[j]:完成j笔交易后,不持有股票的最大利润(即第j次卖出后的最优状态)。

状态转移逻辑:

  • 对于每一天的股价,遍历每一笔交易(从1到k),依次更新buy[j]和sell[j];

  • buy[j] = max(buy[j], sell[j-1] - prices[i]):第j次买入,要么保持之前的持有状态,要么用第j-1次卖出的利润买入当前股票;

  • sell[j] = max(sell[j], buy[j] + prices[i]):第j次卖出,要么保持之前的卖出状态,要么卖出当前持有的股票(用第j次买入的状态计算利润)。

初始化:buy数组初始化为-∞(表示初始状态下,未进行任何交易,无法持有股票),sell数组初始化为0(未进行任何交易,利润为0);buy[0]无需考虑(因为0笔交易无法买入)。

3. 代码解析(最优解法,时间O(nk),空间O(k))

function maxProfit(k: number, prices: number[]): number {
  const len: number = prices.length;
  // 特殊情况:股价数组长度≤1,或k=0,无利润可赚
  if (len <= 1 || k === 0) return 0;
  // 优化:当k≥len/2时,相当于无限次交易,用贪心算法(此处可省略,不影响通用解法,但能提升效率)
  if (k >= len / 2) {
    let profit = 0;
    for (let i = 1; i < len; i++) {
      if (prices[i] > prices[i-1]) profit += prices[i] - prices[i-1];
    }
    return profit;
  }
  // buy[j]:第j次买入后的最大利润(完成j-1笔交易,持有股票)
  // sell[j]:第j次卖出后的最大利润(完成j笔交易,不持有股票)
  const buy: number[] = new Array(k + 1).fill(-Infinity), sell: number[] = new Array(k + 1).fill(0);
  for (let i = 0; i < len; i++) {
    // 遍历每一笔交易,从1到k(j=0无意义,跳过)
    for (let j = 1; j <= k; j++) {
      // 先更新买入状态,再更新卖出状态(顺序不能乱,避免用当天更新的sell[j]计算buy[j])
      buy[j] = Math.max(buy[j], sell[j - 1] - prices[i]);
      sell[j] = Math.max(sell[j], buy[j] + prices[i]);
    }
  }
  // 最多完成k笔交易,sell[k]就是最优利润
  return sell[k];
};

关键细节补充:

  • 特殊情况处理:当len≤1(无交易空间)或k=0(无交易次数),直接返回0;当k≥len/2,用贪心算法(因为每天最多赚一次差价,len天最多len/2笔交易,超过则相当于无限次),提升效率;

  • 数组初始化:buy数组用-∞,因为初始时未进行任何交易,无法持有股票,只有当进行第j次买入时,才会更新为有效利润;sell数组用0,因为未进行任何交易时利润为0;

  • 循环顺序:先遍历每天的股价,再遍历每笔交易,且先更新buy[j]再更新sell[j],避免用当天刚更新的sell[j]去计算buy[j](导致逻辑错误,相当于“同一天又买又卖”)。

4. 示例验证

示例1:k=2,prices = [3,2,6,5,0,3] → 最大利润7

解析:对应123题的场景,第一次在第2天(2)买入,第3天(6)卖出(获利4);第二次在第5天(0)买入,第6天(3)卖出(获利3),总利润7,sell[2] = 7。

示例2:k=1,prices = [7,1,5,3,6,4] → 最大利润5

解析:最多1笔交易,第2天买入(1),第5天卖出(6),获利5,sell[1] = 5。

三、两道题的关联与核心总结

1. 关联点

  • 188题是123题的通用版:当k=2时,188题的解法完全等价于123题,只是123题用变量(buy1、sell1、buy2、sell2)替代了188题的数组(buy[1]、sell[1]、buy[2]、sell[2]),本质都是跟踪“每笔交易的持有/卖出状态”;

  • 核心逻辑一致:都是通过动态规划,每天更新“买入”和“卖出”状态,保证每个状态都是当前最优(取最大值),且严格遵循“先买后卖、先完成前一笔交易再进行下一笔”的规则;

  • 空间优化思路一致:123题用变量优化了188题的数组(k=2时,数组长度为3,可简化为4个变量),本质都是“用有限的空间跟踪有限的状态”。

2. 核心区别

  • 交易次数灵活性:123题固定最多2笔交易,状态数量固定(4个变量);188题支持任意k笔交易,需用数组跟踪k个交易次数的状态;

  • 特殊情况处理:188题需要考虑k≥len/2的情况(简化为无限次交易),而123题k=2,无需考虑此情况(除非len≤4,此时2笔交易已达上限);

  • 空间复杂度:123题是O(1),188题是O(k)(k为交易次数)。

3. 解题通用套路(有限次交易股票问题)

无论是最多2笔、k笔,核心套路都是:

  1. 定义状态:用buy[j]表示“第j次买入后的最大利润”,sell[j]表示“第j次卖出后的最大利润”(j从1到最大交易次数);

  2. 初始化状态:buy数组初始化为-∞(无法初始持有),sell数组初始化为0(无交易利润);

  3. 状态转移:每天遍历股价,依次更新每笔交易的buy[j]和sell[j](先买后卖);

  4. 返回结果:sell[最大交易次数](包含了0到最大交易次数的所有最优情况)。

四、总结

123题和188题是有限次股票交易的典型代表,掌握这两道题,就能举一反三解决所有“最多n笔交易”的股票问题。核心是理解“动态规划跟踪交易状态”的思路——不纠结于具体哪一天买卖,而是通过每天更新状态,自然得到最优解。

建议先吃透123题的变量优化思路,再过渡到188题的数组通用思路,重点关注状态转移的顺序和初始化的逻辑,就能轻松攻克这类题型。如果遇到更复杂的股票问题(如含冷冻期、手续费),也可以基于此思路扩展状态定义即可。