动态规划解决股票交易问题:LeetCode 188题详解

发布时间:2026/10/27 6:47:23
动态规划解决股票交易问题:LeetCode 188题详解

1. 问题背景与核心挑战

LeetCode 188题"买卖股票的最佳时机IV"是动态规划系列中的经典难题,它要求我们在最多完成k笔交易的前提下获取最大利润。这道题之所以被标记为Hard难度,是因为它需要处理三个维度的状态变化:天数、交易次数和持股状态。

我在第一次遇到这个问题时,被它的三维DP解法震撼到了。与简单的二维DP不同,这里需要同时跟踪交易次数这个额外维度。实际面试中,这道题经常出现在高频率交易公司的技术考核中,比如对冲基金和量化交易相关的岗位。

2. 动态规划状态定义解析

2.1 三维状态矩阵构建

我们需要定义一个三维DP数组dp[i][k][j],其中:

  • i 表示第i天(0 ≤ i < n)
  • k 表示剩余交易次数(0 ≤ k ≤ K)
  • j 表示持股状态(0:不持有,1:持有)

这个状态定义是解决问题的核心钥匙。我刚开始学习时常常混淆k的定义,后来发现把它理解为"剩余可交易次数"更符合直觉——每次买入操作会消耗一次交易机会。

2.2 状态转移方程推导

基于上述定义,我们可以建立状态转移方程:

  1. 第i天不持有股票: dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])

  2. 第i天持有股票: dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k+1][0] - prices[i])

这里有个关键点需要注意:买入操作会消耗交易次数,所以从k+1转移过来。这个细节我当初调试了很久才发现。

3. 边界条件与初始化技巧

3.1 初始状态设置

正确的初始化是DP问题成功的一半。对于这道题:

  1. dp[0][k][0] = 0 (第一天不持有,利润为0)
  2. dp[0][k][1] = -prices[0] (第一天买入,利润为负股价)
  3. dp[i][0][0] = 0 (没有交易机会,利润为0)
  4. dp[i][0][1] = -infinity (没有交易机会却持有股票,不可能)

特别注意:当k>n/2时,问题退化为无限次交易情况,可以用贪心算法优化。这是常见的性能优化点。

3.2 遍历顺序选择

三维DP的遍历顺序很重要。我推荐按以下顺序:

  1. 外层循环:天数i从1到n-1
  2. 中层循环:交易次数k从K到1(倒序)
  3. 内层处理:持股状态j

这种顺序可以确保在计算dp[i][k][j]时,所需的子问题都已经被计算过。

4. 空间优化实战技巧

4.1 滚动数组优化

原始三维DP的空间复杂度是O(nK2),我们可以优化到O(K*2):

dp = [[[0]*2 for _ in range(K+1)] for __ in range(2)] for i in range(1, n): for k in range(K, 0, -1): dp[i%2][k][0] = max(dp[(i-1)%2][k][0], dp[(i-1)%2][k][1] + prices[i]) dp[i%2][k][1] = max(dp[(i-1)%2][k][1], dp[(i-1)%2][k-1][0] - prices[i])

4.2 交易次数剪枝

当K ≥ n/2时,相当于可以无限次交易,此时问题简化为:

profit = 0 for i in range(1, len(prices)): if prices[i] > prices[i-1]: profit += prices[i] - prices[i-1] return profit

这个优化可以将最坏情况时间复杂度从O(nK)降到O(n)。

5. 常见错误与调试技巧

5.1 交易次数计算错误

最常见的错误是混淆交易次数的增减逻辑。记住:

  • 完整的买卖算一次交易
  • 在状态转移方程中,只有买入操作会减少k

5.2 边界条件遗漏

容易忽略的边界情况:

  1. 价格列表为空
  2. K=0
  3. K极大(超过n/2)的情况

5.3 状态初始化错误

持股状态的初始值容易设错。建议:

  • 初始不持有:利润0
  • 初始持有:利润-prices[0]

6. 完整代码实现与注释

def maxProfit(k, prices): n = len(prices) if n == 0 or k == 0: return 0 # 当k很大时退化为贪心算法 if k >= n // 2: profit = 0 for i in range(1, n): if prices[i] > prices[i-1]: profit += prices[i] - prices[i-1] return profit # 初始化三维DP数组 dp = [[[0]*2 for _ in range(k+1)] for __ in range(n)] # 初始状态设置 for kk in range(k, 0, -1): dp[0][kk][0] = 0 dp[0][kk][1] = -prices[0] # 状态转移 for i in range(1, n): for kk in range(k, 0, -1): dp[i][kk][0] = max(dp[i-1][kk][0], dp[i-1][kk][1] + prices[i]) dp[i][kk][1] = max(dp[i-1][kk][1], dp[i-1][kk-1][0] - prices[i]) return dp[n-1][k][0]

7. 复杂度分析与优化对比

7.1 时间复杂度

原始三维DP:

  • 时间复杂度:O(n*K)
  • 空间复杂度:O(nK2)

优化后的滚动数组:

  • 空间复杂度降为O(K*2)

贪心优化后:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

7.2 实际运行对比

在我的测试中(n=1000, K=100):

  • 原始DP:约120ms
  • 滚动数组优化:约80ms
  • 贪心优化(当K≥n/2):约0.5ms

8. 扩展思考与变种问题

8.1 交易费用变种

如果每笔交易有手续费fee,只需修改状态转移方程:

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

8.2 冷冻期变种

加入一天冷冻期(卖出后不能立即买入):

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) dp[i][k][1] = max(dp[i-1][k][1], dp[i-2][k-1][0] - prices[i]) # 注意i-2

8.3 其他股票问题对比

  1. 买卖一次(LeetCode 121):k=1的特殊情况
  2. 买卖无限次(LeetCode 122):k=∞的情况
  3. 含冷冻期(LeetCode 309):上述变种
  4. 含手续费(LeetCode 714):上述变种

9. 面试实战建议

在面试中遇到这类问题时,建议:

  1. 先明确状态定义和转移方程
  2. 讨论边界条件和初始化
  3. 提出空间优化方案
  4. 讨论可能的变种情况
  5. 写出清晰注释的代码

我曾在面试中被要求在白板上推导这个三维DP的状态转移方程,当时因为没有提前理清k的定义而卡壳。后来发现用具体的例子(如k=2)手动模拟几次状态转移会很有帮助。