1. 项目概述
最近在算法训练营中遇到了经典的股票买卖问题系列,特别是LeetCode上的121、122和123这三道题目。作为动态规划领域的经典案例,这些题目不仅考察我们对状态转移的理解,更考验我们如何将现实问题抽象为算法模型的能力。在实际操作中,我发现很多同学(包括我自己最初)都会陷入"暴力解法"的思维定式,而忽略了动态规划的精妙之处。
这三道题目看似相似,实则各有特点:121题限制只能买卖一次,122题允许无限次交易,而123题则限制最多两次交易。这种渐进式的难度设计,恰好反映了动态规划问题从简单到复杂的思考路径。通过系统性地解决这个系列,我们能够建立起处理更复杂动态规划问题的思维框架。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心思路解析
2.1 动态规划在股票问题中的应用
股票买卖问题本质上是最优化问题——在给定的价格序列中,找到使利润最大化的买卖策略。动态规划之所以适合这类问题,是因为它具有以下特点:
- 最优子结构:最终的最大利润取决于之前每一天的最优决策
- 重叠子问题:计算第i天的状态时,会重复用到之前的状态值
- 无后效性:未来的决策只依赖于当前状态,不依赖于过去如何到达当前状态
对于这类问题,我们通常需要定义状态转移方程。关键在于准确描述"状态"——在股票问题中,状态通常包括:
- 当前是第几天
- 还允许进行多少次交易
- 当前是否持有股票
2.2 三道题目的共性与差异
虽然三道题目都涉及股票买卖,但它们的约束条件不同,导致状态定义和转移方程有所差异:
| 题目 | 交易次数限制 | 状态维度 | 主要区别 |
|---|---|---|---|
| 121 | 最多1次 | 2 | 只能买卖一次 |
| 122 | 无限次 | 2 | 可以多次买卖 |
| 123 | 最多2次 | 3 | 限制交易次数 |
理解这些差异对设计正确的状态转移方程至关重要。接下来,我们将分别深入每道题目的解法。
3. 121. 买卖股票的最佳时机
3.1 问题描述
给定一个数组 prices,其中 prices[i] 表示某支股票第 i 天的价格。你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
示例:
输入:[7,1,5,3,6,4]
输出:5
解释:在第2天买入(价格=1),第5天卖出(价格=6),利润=6-1=5。
3.2 暴力解法分析
最直观的解法是双重循环:
python复制def maxProfit(prices):
max_profit = 0
for i in range(len(prices)):
for j in range(i+1, len(prices)):
profit = prices[j] - prices[i]
if profit > max_profit:
max_profit = profit
return max_profit
时间复杂度O(n²),空间复杂度O(1)。对于大规模数据效率太低。
3.3 动态规划解法
我们可以将问题转化为:找到历史最低点,然后在之后的每一天计算如果今天卖出能获得的最大利润。
定义状态:
- min_price: 前i天的最低价格
- max_profit: 前i天的最大利润
状态转移:
- min_price = min(min_price, prices[i])
- max_profit = max(max_profit, prices[i] - min_price)
实现代码:
python复制def maxProfit(prices):
min_price = float('inf')
max_profit = 0
for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
时间复杂度O(n),空间复杂度O(1)。
注意:这里虽然用了动态规划的思想,但实际实现可以简化为一次遍历。这是121题的特殊之处,因为状态转移只依赖前一个状态。
4. 122. 买卖股票的最佳时机 II
4.1 问题描述
给你一个整数数组 prices,其中 prices[i] 表示某支股票第 i 天的价格。你可以尽可能地完成更多的交易(多次买卖一支股票),但不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。
示例:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:第2天买入,第3天卖出,利润=5-1=4;第4天买入,第5天卖出,利润=6-3=3。总利润=4+3=7。
4.2 贪心算法解法
这道题的一个巧妙解法是贪心算法:只要今天的价格比昨天高,就进行交易。
python复制def maxProfit(prices):
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
时间复杂度O(n),空间复杂度O(1)。
4.3 动态规划解法
为了与后续题目保持解法一致性,我们也可以用动态规划来解决。
定义状态:
- dp[i][0]: 第i天结束时未持有股票的最大利润
- dp[i][1]: 第i天结束时持有股票的最大利润
初始状态:
- dp[0][0] = 0
- dp[0][1] = -prices[0]
状态转移:
- 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])
实现代码:
python复制def maxProfit(prices):
n = len(prices)
dp = [[0]*2 for _ in range(n)]
dp[0][0] = 0
dp[0][1] = -prices[0]
for i in range(1, n):
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])
return dp[n-1][0]
可以优化空间复杂度到O(1):
python复制def maxProfit(prices):
hold, not_hold = -prices[0], 0
for price in prices[1:]:
prev_hold, prev_not_hold = hold, not_hold
hold = max(prev_hold, prev_not_hold - price)
not_hold = max(prev_not_hold, prev_hold + price)
return not_hold
5. 123. 买卖股票的最佳时机 III
5.1 问题描述
给定一个数组 prices,其中 prices[i] 表示某支股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔交易。
示例:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:第4天买入,第6天卖出,利润=3-0=3;第7天买入,第8天卖出,利润=4-1=3。总利润=3+3=6。
5.2 动态规划解法
这道题需要更复杂的状态定义,因为交易次数受限。
定义状态:
- dp[i][k][0]: 第i天结束时,最多进行了k次交易,未持有股票的最大利润
- dp[i][k][1]: 第i天结束时,最多进行了k次交易,持有股票的最大利润
对于本题,k最大为2。
初始状态:
- dp[0][k][0] = 0 (k=0,1,2)
- dp[0][k][1] = -prices[0] (k=0,1,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-1][k-1][0] - prices[i])
实现代码:
python复制def maxProfit(prices):
n = len(prices)
if n == 0:
return 0
max_k = 2
dp = [[[0]*2 for _ in range(max_k+1)] for _ in range(n)]
for k in range(max_k+1):
dp[0][k][0] = 0
dp[0][k][1] = -prices[0]
for i in range(1, n):
for k in range(1, max_k+1):
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])
return dp[n-1][max_k][0]
5.3 空间优化
由于状态只依赖前一天的值,可以优化空间复杂度:
python复制def maxProfit(prices):
n = len(prices)
if n == 0:
return 0
# 初始化第一次和第二次交易的状态
first_buy = second_buy = -prices[0]
first_sell = second_sell = 0
for price in prices[1:]:
first_buy = max(first_buy, -price)
first_sell = max(first_sell, first_buy + price)
second_buy = max(second_buy, first_sell - price)
second_sell = max(second_sell, second_buy + price)
return second_sell
6. 常见问题与优化技巧
6.1 边界条件处理
在实际编码中,有几个边界条件需要特别注意:
- 空输入处理:当prices数组为空时,直接返回0
- 单调递减序列:当价格一直下跌时,最大利润应为0(不交易)
- 单元素输入:无法完成交易,返回0
6.2 状态压缩技巧
从上面的实现可以看出,动态规划的状态通常只依赖前一个状态,因此可以压缩空间复杂度:
- 121题:O(1)空间
- 122题:O(1)空间
- 123题:从O(n)优化到O(1)空间
6.3 通用解法框架
通过这三道题目,我们可以总结出一个处理股票问题的通用框架:
- 定义状态:通常包括天数、交易次数限制、持有状态
- 初始化:第0天的各种状态值
- 状态转移:根据题目约束写出转移方程
- 返回结果:通常是最后一天不持有股票的最大利润
对于最多k次交易的情况,可以写出如下通用解法:
python复制def maxProfit(k, prices):
n = len(prices)
if n == 0:
return 0
if k > n//2:
# 相当于无限次交易,用贪心解法
return sum(max(prices[i]-prices[i-1],0) for i in range(1,n))
dp = [[[0]*2 for _ in range(k+1)] for _ in range(n)]
for j in range(k+1):
dp[0][j][0] = 0
dp[0][j][1] = -prices[0]
for i in range(1, n):
for j in range(1, k+1):
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
return dp[n-1][k][0]
6.4 调试技巧
在实现动态规划时,可以:
- 打印dp表,观察状态变化
- 从小例子开始,手动计算验证
- 特别注意索引边界(如从1开始还是从0开始)
7. 扩展与变种
掌握了这三道基础题目后,可以尝试解决更复杂的变种:
- 买卖股票的最佳时机 IV(任意k次交易)
- 含冷冻期的买卖股票问题(卖出后需要等待一天)
- 含手续费的买卖股票问题
- 买卖股票的最佳时机含交易费用
这些变种都可以在现有框架基础上,通过调整状态定义和转移方程来解决。例如,含冷冻期的问题需要额外记录冷冻状态,含手续费的问题需要在每次交易时扣除手续费。
