1. 动态规划解决股票买卖问题的核心思路
股票买卖问题是算法面试中的经典题型,也是动态规划应用的典型案例。这类问题的核心在于如何在给定的价格序列中找到最优的买卖时机,使得利润最大化。不同的问题变体会对交易次数、交易间隔等做出限制,这就需要我们灵活调整动态规划的状态定义和转移方程。
动态规划之所以适合解决这类问题,是因为它具有以下特点:
- 问题可以分解为多个阶段(每天的价格就是一个阶段)
- 每个阶段有多种状态(持有/不持有股票)
- 当前阶段的最优解可以由前一阶段的最优解推导出来
- 需要记录中间状态以避免重复计算
2. 只能买卖一次的情况(121题)
2.1 问题分析与状态定义
这是最基本的股票买卖问题,限制条件是最多只能完成一笔交易(买一次,卖一次)。我们需要找到一个最低点买入,然后在之后的某个最高点卖出。
定义状态:
- dp[i][0]:第i天结束时持有股票的最大利润
- dp[i][1]:第i天结束时不持有股票的最大利润
注意这里的"持有"不一定代表当天买入,可能是之前买入后一直保持持有状态。
2.2 状态转移方程推导
对于第i天持有股票的情况(dp[i][0]),有两种可能:
- 第i-1天就已经持有股票,今天保持不动:dp[i-1][0]
- 第i天买入股票:-prices[i](因为是第一次买入,之前没有利润)
所以转移方程为:
dp[i][0] = max(dp[i-1][0], -prices[i])
对于第i天不持有股票的情况(dp[i][1]),也有两种可能:
- 第i-1天就不持有股票,今天保持不动:dp[i-1][1]
- 第i天卖出股票:prices[i] + dp[i-1][0]
所以转移方程为:
dp[i][1] = max(dp[i-1][1], prices[i] + dp[i-1][0])
2.3 初始化与边界条件
初始状态需要特别处理:
- dp[0][0] = -prices[0](第一天买入)
- dp[0][1] = 0(第一天不买入)
2.4 代码实现与优化
java复制public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], -prices[i]);
dp[i][1] = Math.max(dp[i-1][1], prices[i] + dp[i-1][0]);
}
return dp[n-1][1];
}
空间优化:由于每天的状态只依赖于前一天的状态,可以用两个变量代替二维数组:
java复制public int maxProfit(int[] prices) {
int hold = -prices[0], notHold = 0;
for (int i = 1; i < prices.length; i++) {
hold = Math.max(hold, -prices[i]);
notHold = Math.max(notHold, prices[i] + hold);
}
return notHold;
}
3. 可以无限次买卖的情况(122题)
3.1 问题变化与调整思路
与121题相比,122题允许进行多次交易,但同一时间只能持有一支股票。这意味着我们可以在获得利润后再次投资。
状态定义与121题相同,但状态转移方程需要调整,因为在买入时可能已经有之前的利润。
3.2 状态转移方程调整
关键变化在于买入时的计算:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])
这里dp[i-1][1] - prices[i]表示用之前积累的利润来购买新的股票。
3.3 代码实现
java复制public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
dp[i][1] = Math.max(dp[i-1][1], prices[i] + dp[i-1][0]);
}
return dp[n-1][1];
}
空间优化版本:
java复制public int maxProfit(int[] prices) {
int hold = -prices[0], notHold = 0;
for (int price : prices) {
int prevHold = hold;
hold = Math.max(hold, notHold - price);
notHold = Math.max(notHold, price + prevHold);
}
return notHold;
}
4. 最多买卖两次的情况(123题)
4.1 复杂状态定义
当限制最多完成两笔交易时,我们需要更细致地跟踪交易状态。定义五种状态:
- 无操作
- 第一次持有股票
- 第一次不持有股票(完成第一次交易)
- 第二次持有股票
- 第二次不持有股票(完成第二次交易)
4.2 状态转移方程
每种状态的转移:
- 无操作:保持前一天状态
- 第一次持有:max(保持第一次持有,从无操作状态买入)
- 第一次不持有:max(保持第一次不持有,卖出第一次持有的股票)
- 第二次持有:max(保持第二次持有,用第一次交易的利润买入)
- 第二次不持有:max(保持第二次不持有,卖出第二次持有的股票)
4.3 代码实现
java复制public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][5];
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i-1][0];
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + prices[i]);
dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] - prices[i]);
dp[i][4] = Math.max(dp[i-1][4], dp[i-1][3] + prices[i]);
}
return dp[n-1][4];
}
空间优化版本:
java复制public int maxProfit(int[] prices) {
int s0 = 0;
int s1 = -prices[0];
int s2 = 0;
int s3 = -prices[0];
int s4 = 0;
for (int i = 1; i < prices.length; i++) {
s1 = Math.max(s1, s0 - prices[i]);
s2 = Math.max(s2, s1 + prices[i]);
s3 = Math.max(s3, s2 - prices[i]);
s4 = Math.max(s4, s3 + prices[i]);
}
return s4;
}
5. 常见问题与优化技巧
5.1 边界条件处理
- 空输入检查:如果prices为空或长度小于2,直接返回0
- 单调递减情况:如果价格一直下跌,应该不进行任何交易
5.2 空间复杂度优化
所有股票问题都可以将空间复杂度从O(n)优化到O(1),因为每天的状态只依赖于前一天的状态。在实际面试中,建议先写出标准的二维DP解法,然后再优化空间。
5.3 调试技巧
- 打印DP表格:对于小样例,打印出整个DP表格有助于验证状态转移是否正确
- 测试极端情况:如价格全部相同、单调递增/递减等
5.4 其他变种问题
掌握了这三种基本变种后,可以解决更复杂的股票问题:
- 含冷冻期(卖出后需要等待一天才能买入)
- 含手续费(每笔交易需要支付固定费用)
- 限制交易次数为k次(通用解法)
6. 实际应用中的思考
在实际投资中,这些算法模型虽然简化了很多现实因素,但核心思想非常有用:
- 分阶段决策:将投资过程分解为每天的决策
- 状态跟踪:清晰记录当前的投资状态
- 最优子结构:当前最优决策基于之前的最优决策
我在实际编码面试中经常遇到这类问题的变种,理解状态定义和转移方程的设计思路比记忆具体代码更重要。建议从最简单的只能买卖一次的情况开始,逐步增加复杂度,体会状态设计的变化规律。