1. 股票买卖问题概述
股票买卖问题是算法面试和编程竞赛中的经典题型,也是动态规划应用的绝佳场景。这类问题通常给定一个股票价格序列,要求在不同约束条件下计算能够获得的最大利润。看似简单的买卖操作背后,隐藏着对状态转移和最优子结构的深刻理解。
在实际操作中,我发现这类问题有以下几个共同特点:
- 价格序列通常以数组形式给出,每个元素代表某天的股票价格
- 买卖操作受到各种限制(如交易次数、冷冻期、手续费等)
- 需要找到在给定约束下的最优买卖策略
2. 基础问题解析:单次交易
2.1 问题描述与暴力解法
LeetCode 121题是最基础的股票买卖问题:给定一个数组prices,其中prices[i]表示第i天股票的价格。你只能选择某一天买入,并在未来某一天卖出,计算最大利润。
最直观的暴力解法是双重循环:
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)。这种方法在小数据量时可行,但面对大规模数据效率低下。
2.2 动态规划解法
更高效的解法是动态规划。定义状态:
- dp[i][0]:第i天持有股票时的最大利润
- dp[i][1]:第i天不持有股票时的最大利润
状态转移方程:
python复制dp[i][0] = max(dp[i-1][0], -prices[i]) # 保持或买入
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) # 保持或卖出
初始化:
python复制dp[0][0] = -prices[0] # 第一天买入
dp[0][1] = 0 # 第一天不持有
完整实现:
python复制def maxProfit(prices):
n = len(prices)
dp = [[0]*2 for _ in range(n)]
dp[0][0] = -prices[0]
for i in range(1, n):
dp[i][0] = max(dp[i-1][0], -prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i])
return dp[-1][1]
2.3 空间优化
观察状态转移发现,当前状态只依赖前一个状态,可以优化空间:
python复制def maxProfit(prices):
hold, not_hold = -prices[0], 0
for price in prices[1:]:
hold = max(hold, -price)
not_hold = max(not_hold, hold + price)
return not_hold
时间复杂度O(n),空间复杂度O(1)。
3. 多次交易问题
3.1 无限次交易(LeetCode 122)
当允许无限次买卖时,策略变为在每一个上升波段低买高卖。动态规划状态定义与单次交易相同,但买入时的资金来自之前交易的利润。
状态转移:
python复制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):
hold, not_hold = -prices[0], 0
for price in prices[1:]:
hold = max(hold, not_hold - price)
not_hold = max(not_hold, hold + price)
return not_hold
3.2 最多两次交易(LeetCode 123)
这个问题引入了交易次数限制。我们需要跟踪每次交易的状态:
状态定义:
- 0:未操作
- 1:第一次买入
- 2:第一次卖出
- 3:第二次买入
- 4:第二次卖出
状态转移方程:
python复制dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i])
dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i])
dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i])
实现代码:
python复制def maxProfit(prices):
dp = [0]*5
dp[1] = dp[3] = -prices[0]
for price in prices[1:]:
dp[1] = max(dp[1], dp[0] - price)
dp[2] = max(dp[2], dp[1] + price)
dp[3] = max(dp[3], dp[2] - price)
dp[4] = max(dp[4], dp[3] + price)
return dp[4]
3.3 最多k次交易(LeetCode 188)
将两次交易推广到k次,状态数为2k+1(包含初始状态)。使用循环处理状态转移:
python复制def maxProfit(k, prices):
if not prices: return 0
n = len(prices)
if k >= n//2: # 相当于无限次交易
return sum(max(0, prices[i]-prices[i-1]) for i in range(1,n))
dp = [0]*(2*k+1)
for i in range(1, 2*k+1, 2):
dp[i] = -prices[0]
for price in prices[1:]:
for j in range(1, 2*k+1):
if j % 2 == 1: # 买入状态
dp[j] = max(dp[j], dp[j-1] - price)
else: # 卖出状态
dp[j] = max(dp[j], dp[j-1] + price)
return dp[-1]
4. 复杂条件问题
4.1 含冷冻期(LeetCode 309)
冷冻期要求卖出后必须等待一天才能买入。我们需要增加一个状态表示冷冻期后的可买入状态:
状态定义:
- 持有股票
- 不持有股票且在冷冻期
- 不持有股票且不在冷冻期
状态转移:
python复制def maxProfit(prices):
hold, cool, not_hold = -prices[0], 0, 0
for price in prices[1:]:
new_hold = max(hold, not_hold - price)
new_cool = hold + price
new_not_hold = max(not_hold, cool)
hold, cool, not_hold = new_hold, new_cool, new_not_hold
return max(cool, not_hold)
4.2 含手续费(LeetCode 714)
每次卖出时扣除手续费,只需在卖出状态转移时减去fee:
python复制def maxProfit(prices, fee):
hold, not_hold = -prices[0], 0
for price in prices[1:]:
hold = max(hold, not_hold - price)
not_hold = max(not_hold, hold + price - fee)
return not_hold
5. 实战技巧与注意事项
-
状态定义是关键:不同问题需要设计不同的状态表示,理解每个状态的实际含义至关重要
-
初始化陷阱:持有股票的初始状态通常是-price[0],但要根据具体问题调整
-
空间优化:大多数问题都可以优化到O(1)空间,但初学时可先写出二维DP再优化
-
边界条件:空输入、单日价格等特殊情况需要单独处理
-
调试技巧:打印DP表格有助于理解状态转移过程
在实际面试中,建议先明确问题约束条件,然后设计状态转移方程,最后考虑空间优化。这类问题的核心在于对状态转移的理解,多练习不同变种能够帮助建立解题直觉。