1. 动态规划在股票买卖问题中的实战应用
作为一名经历过多次算法面试毒打的程序员,我深知股票买卖系列问题在面试中的高频出现率。今天要讨论的这三道力扣题目(188. 买卖股票的最佳时机 IV、309. 最佳买卖股票时机含冷冻期、714. 买卖股票的最佳时机含手续费)可以说是动态规划应用的经典案例。它们看似相似,实则各有特点,需要我们对状态转移方程有深刻理解。
1.1 问题共性分析
这三个问题都属于股票买卖问题的变种,核心都是通过动态规划来寻找最优解。它们共享以下特征:
- 给定一个股票价格数组 prices
- 限制交易次数或添加额外条件
- 目标是最大化利润
不同之处在于约束条件:
- 188题限制最多完成k笔交易
- 309题要求卖出后有一天冷冻期
- 714题则对每笔交易收取固定手续费
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 188. 买卖股票的最佳时机 IV
2.1 问题重述
给定一个整数数组 prices 和一个整数 k,其中 prices[i] 是某支股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易(买入和卖出算一次交易)。
2.2 动态规划状态定义
这是股票系列中最通用的情况,我们需要定义一个三维DP数组:
- dp[i][k][0]:第i天,最多进行了k次交易,手中没有股票时的最大利润
- dp[i][k][1]:第i天,最多进行了k次交易,手中有股票时的最大利润
状态转移方程:
code复制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-1][k-1][0] - prices[i])
2.3 代码实现与优化
由于三维DP可能消耗较多内存,我们可以优化为二维数组:
python复制def maxProfit(k, prices):
if not prices:
return 0
n = len(prices)
if k >= n // 2:
# 相当于可以无限次交易
max_profit = 0
for i in range(1, n):
if prices[i] > prices[i-1]:
max_profit += prices[i] - prices[i-1]
return max_profit
dp = [[0] * n for _ in range(k+1)]
for i in range(1, k+1):
max_diff = -prices[0]
for j in range(1, n):
dp[i][j] = max(dp[i][j-1], prices[j] + max_diff)
max_diff = max(max_diff, dp[i-1][j] - prices[j])
return dp[k][n-1]
关键点:当k >= n/2时,问题退化为无限次交易的情况,可以直接贪心解决,避免不必要的DP计算。
3. 309. 最佳买卖股票时机含冷冻期
3.1 问题特点分析
这道题新增了冷冻期的限制:在卖出股票后,你无法在第二天买入股票(即冷冻期为1天)。这个约束条件改变了状态转移的逻辑。
3.2 状态定义与转移
我们需要定义三个状态:
- hold:持有股票时的最大利润
- sold:当天卖出股票后的最大利润
- rest:处于冷冻期或什么都不做的最大利润
状态转移方程:
code复制hold[i] = max(hold[i-1], rest[i-1] - prices[i])
sold[i] = hold[i-1] + prices[i]
rest[i] = max(rest[i-1], sold[i-1])
3.3 代码实现
python复制def maxProfit(prices):
if not prices:
return 0
n = len(prices)
hold = [0] * n
sold = [0] * n
rest = [0] * n
hold[0] = -prices[0]
for i in range(1, n):
hold[i] = max(hold[i-1], rest[i-1] - prices[i])
sold[i] = hold[i-1] + prices[i]
rest[i] = max(rest[i-1], sold[i-1])
return max(sold[-1], rest[-1])
实际编码时可以进一步优化空间复杂度,只保留前一个状态的值即可。
4. 714. 最佳买卖股票时机含手续费
4.1 问题特点
这道题的特点是每笔交易都需要支付固定的手续费,这影响了我们的买卖决策。我们需要在卖出时考虑手续费的影响。
4.2 状态定义
定义两个状态:
- cash:不持有股票时的最大利润
- hold:持有股票时的最大利润
状态转移方程:
code复制cash = max(cash, hold + prices[i] - fee)
hold = max(hold, cash - prices[i])
4.3 代码实现
python复制def maxProfit(prices, fee):
if not prices:
return 0
cash, hold = 0, -prices[0]
for i in range(1, len(prices)):
cash = max(cash, hold + prices[i] - fee)
hold = max(hold, cash - prices[i])
return cash
注意:手续费只在卖出时收取一次,所以只需要在cash状态更新时减去fee。
5. 股票问题通用解法框架
通过这三道题目,我们可以总结出一个解决股票买卖问题的通用框架:
5.1 状态定义
对于大多数股票问题,我们都可以定义以下状态:
- dp[i][k][0]:第i天,最多k次交易,不持有股票
- dp[i][k][1]:第i天,最多k次交易,持有股票
5.2 基础状态转移方程
code复制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-1][k-1][0] - prices[i])
5.3 根据约束条件调整
- 交易次数限制:调整k的范围
- 冷冻期:增加额外状态
- 手续费:在卖出或买入时扣除
6. 常见错误与调试技巧
6.1 边界条件处理
- 空输入检查:总是首先检查prices是否为空
- 初始化:hold状态的初始值应为-prices[0]
- 交易次数为0时的处理
6.2 空间优化技巧
很多股票问题都可以将空间复杂度从O(n)优化到O(1),只需保存前一个状态的值:
python复制prev_cash, prev_hold = 0, -prices[0]
for price in prices[1:]:
curr_cash = max(prev_cash, prev_hold + price)
curr_hold = max(prev_hold, prev_cash - price)
prev_cash, prev_hold = curr_cash, curr_hold
6.3 调试建议
- 打印DP表格:对于复杂问题,打印中间DP值有助于理解
- 小规模测试:先用小的测试案例验证
- 状态跟踪:记录每个状态的变化过程
7. 性能优化实战
7.1 188题的特殊情况处理
当k >= n/2时,问题等同于无限次交易,可以直接用贪心算法:
python复制if k >= len(prices) // 2:
return sum(max(prices[i] - prices[i-1], 0) for i in range(1, len(prices)))
这种优化可以将时间复杂度从O(nk)降到O(n)。
7.2 309题的状态合并
实际上,309题可以简化为两个状态:
- hold:持有股票
- not_hold:不持有股票(包括冷冻期和非冷冻期)
python复制def maxProfit(prices):
hold, not_hold = -float('inf'), 0
prev_not_hold = 0
for price in prices:
hold = max(hold, prev_not_hold - price)
prev_not_hold = not_hold
not_hold = max(not_hold, hold + price)
return not_hold
8. 面试实战建议
8.1 解题思路
- 明确问题约束条件
- 定义合适的状态
- 推导状态转移方程
- 确定初始条件
- 考虑空间优化
8.2 沟通技巧
- 先解释状态定义
- 再推导转移方程
- 讨论边界条件
- 最后考虑优化
8.3 常见变种准备
- 交易次数限制
- 冷冻期
- 手续费
- 买入/卖出限制
- 多支股票选择
9. 个人实战心得
在实际刷题和面试中,我发现股票问题最关键的几点:
- 状态定义要清晰:明确每个状态代表什么含义
- 转移方程要准确:考虑所有可能的转移路径
- 初始条件要正确:特别是hold状态的初始值
- 空间优化要谨慎:先写出完整DP,再考虑优化
对于冷冻期问题,我最初常常混淆rest和sold状态的关系。后来通过画状态机图才彻底理解:卖出后必须进入冷冻期,冷冻期结束后才能买入。
手续费问题则要注意是在买入还是卖出时扣除。不同扣除时机会影响状态转移方程。我建议统一在卖出时扣除,这样更符合实际交易场景。
