引言
在动态规划系列的前五篇文章中,我们分别学习了爬楼梯问题、最长公共子序列、01背包问题、最长递增子序列和不同路径问题。今天我们将探讨动态规划中极具实用价值的一类问题——股票买卖问题。
股票买卖问题是动态规划中的经典应用,它不仅在算法面试中频繁出现,更与实际的投资决策密切相关。通过学习这类问题,我们将深入理解状态机动态规划的设计思想和实现技巧。
问题背景
股票买卖问题的核心在于:在给定的股票价格序列中,通过买入和卖出操作获得最大利润。
这类问题通常具有以下特征:
- 时间序列:股票价格按时间顺序给出
- 操作限制:买入卖出有各种限制条件
- 状态转换:持有股票和不持有股票是两种不同的状态
- 最优化目标:追求最大利润
问题分类与解法
股票买卖问题根据交易限制可以分为几大类,我们将逐一分析:
类型一:买卖股票的最佳时机(只能交易一次)
LeetCode 121. 买卖股票的最佳时机
问题描述
给定一个数组 prices,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择某一天买入这只股票,并选择在未来的某一天卖出该股票。设计一个算法来计算你所能获取的最大利润。
1 2 3 4
| 示例 1: 输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
|
解法一:暴力法
1 2 3 4 5 6 7 8 9 10
| public int maxProfit(int[] prices) { int maxProfit = 0; for (int i = 0; i < prices.length; i++) { for (int j = i + 1; j < prices.length; j++) { int profit = prices[j] - prices[i]; maxProfit = Math.max(maxProfit, profit); } } return maxProfit; }
|
时间复杂度:O(n²),空间复杂度:O(1)
解法二:一次遍历(贪心思想)
核心思想:在每一天,我们都尝试以当前价格卖出,同时维护到目前为止的最低买入价格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public int maxProfit(int[] prices) { int minPrice = Integer.MAX_VALUE; int maxProfit = 0; for (int price : prices) { if (price < minPrice) { minPrice = price; } else if (price - minPrice > maxProfit) { maxProfit = price - minPrice; } } return maxProfit; }
|
时间复杂度:O(n),空间复杂度:O(1)
解法三:动态规划(状态机思想)
状态定义:
dp[i][0]:第 i 天不持有股票的最大利润
dp[i][1]:第 i 天持有股票的最大利润
状态转移方程:
1 2
| dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) // 不持有:保持不持有 或 卖出 dp[i][1] = max(dp[i-1][1], -prices[i]) // 持有:保持持有 或 买入
|
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public int maxProfit(int[] prices) { int n = prices.length; if (n <= 1) return 0; int[][] dp = new int[n][2]; dp[0][0] = 0; dp[0][1] = -prices[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]); } return dp[n-1][0]; }
|
空间优化版本:
1 2 3 4 5 6 7 8 9 10 11
| public int maxProfit(int[] prices) { int sold = 0; int hold = -prices[0]; for (int i = 1; i < prices.length; i++) { sold = Math.max(sold, hold + prices[i]); hold = Math.max(hold, -prices[i]); } return sold; }
|
类型二:买卖股票的最佳时机II(无限次交易)
LeetCode 122. 买卖股票的最佳时机 II
问题描述(无限次交易)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
1 2 3 4 5
| 示例: 输入: [7,1,5,3,6,4] 输出: 7 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3。
|
解法一:贪心算法
核心思想:只要后一天的价格比前一天高,就进行交易。
1 2 3 4 5 6 7 8 9
| public int maxProfit(int[] prices) { int maxProfit = 0; for (int i = 1; i < prices.length; i++) { if (prices[i] > prices[i-1]) { maxProfit += prices[i] - prices[i-1]; } } return maxProfit; }
|
时间复杂度:O(n),空间复杂度:O(1)
解法二:动态规划
状态转移方程:
1 2
| 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]) // 注意这里可以多次买入
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public int maxProfit(int[] prices) { int sold = 0; int hold = -prices[0]; for (int i = 1; i < prices.length; i++) { int newSold = Math.max(sold, hold + prices[i]); int newHold = Math.max(hold, sold - prices[i]); sold = newSold; hold = newHold; } return sold; }
|
类型三:买卖股票的最佳时机III(最多2次交易)
LeetCode 123. 买卖股票的最佳时机 III
问题描述(最多2次交易)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔交易。
解法:动态规划
状态定义:
buy1:第一次买入后的最大利润
sell1:第一次卖出后的最大利润
buy2:第二次买入后的最大利润
sell2:第二次卖出后的最大利润
1 2 3 4 5 6 7 8 9 10 11 12 13
| public int maxProfit(int[] prices) { int buy1 = -prices[0], sell1 = 0; int buy2 = -prices[0], sell2 = 0; for (int i = 1; i < prices.length; i++) { buy1 = Math.max(buy1, -prices[i]); sell1 = Math.max(sell1, buy1 + prices[i]); buy2 = Math.max(buy2, sell1 - prices[i]); sell2 = Math.max(sell2, buy2 + prices[i]); } return sell2; }
|
类型四:买卖股票的最佳时机IV(最多k次交易)
LeetCode 188. 买卖股票的最佳时机 IV
问题描述(最多k次交易)
给定一个整数数组 prices 和一个整数 k,其中 prices[i] 是第 i 天的股票价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
解法:动态规划(k次交易)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public int maxProfit(int k, int[] prices) { int n = prices.length; if (n <= 1 || k == 0) return 0; if (k >= n / 2) { int maxProfit = 0; for (int i = 1; i < n; i++) { if (prices[i] > prices[i-1]) { maxProfit += prices[i] - prices[i-1]; } } return maxProfit; } int[][][] dp = new int[n][k+1][2]; for (int j = 1; j <= k; j++) { dp[0][j][0] = 0; dp[0][j][1] = -prices[0]; } for (int i = 1; i < n; i++) { for (int j = 1; j <= k; j++) { dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]); dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]); } } return dp[n-1][k][0]; }
|
类型五:买卖股票的最佳时机含冷冻期
LeetCode 309. 买卖股票的最佳时机含冷冻期
问题描述(含冷冻期)
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
- 卖出股票后,你无法在第二天买入股票(即冷冻期为1天)
解法:动态规划(含冷冻期)
状态定义:
hold:持有股票的最大利润
sold:刚卖出股票的最大利润(处于冷冻期)
rest:不持有股票且不在冷冻期的最大利润
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public int maxProfit(int[] prices) { if (prices.length <= 1) return 0; int hold = -prices[0]; int sold = 0; int rest = 0; for (int i = 1; i < prices.length; i++) { int prevHold = hold; int prevSold = sold; int prevRest = rest; hold = Math.max(prevHold, prevRest - prices[i]); sold = prevHold + prices[i]; rest = Math.max(prevRest, prevSold); } return Math.max(sold, rest); }
|
通用解法框架
通过分析上述问题,我们可以总结出股票买卖问题的通用解法框架:
状态机设计
1 2 3 4 5 6 7 8
| 状态定义:dp[i][k][s] - i:第i天 - k:最多k次交易 - s:当前状态(0=不持有,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]) // 买入
|
边界条件处理
1 2 3
| base case: dp[0][k][0] = 0 // 第0天不持有股票,利润为0 dp[0][k][1] = -prices[0] // 第0天持有股票,利润为-prices[0]
|
空间优化策略
由于状态只依赖于前一天,通常可以将空间复杂度从 O(nk) 优化到 O(k)。
解题技巧总结
1. 问题转化
- 将复杂的交易限制转化为状态机
- 明确每个状态的含义和转移条件
2. 特殊情况优化
- 当 k ≥ n/2 时,相当于无限次交易
- 对于冷冻期,增加中间状态处理
3. 空间优化
实际应用价值
股票买卖问题不仅是算法问题,还具有重要的实际应用价值:
1. 投资策略分析
- 帮助理解不同交易策略的收益上限
- 为量化交易提供理论基础
2. 算法思维训练
3. 面试准备
复杂度分析
| 问题类型 |
时间复杂度 |
空间复杂度 |
主要思想 |
| 买卖一次 |
O(n) |
O(1) |
贪心/DP |
| 无限次交易 |
O(n) |
O(1) |
贪心 |
| 最多k次 |
O(nk) |
O(k) |
DP |
| 含冷冻期 |
O(n) |
O(1) |
状态机DP |
总结
股票买卖问题是动态规划中的经典应用,它完美地展现了状态机动态规划的设计思想:
- 状态设计巧妙:通过持有/不持有股票来划分状态
- 转移逻辑清晰:买入、卖出、持有三种操作对应不同的状态转移
- 优化策略多样:从暴力到DP,从二维到一维的优化过程
- 实用价值高:既是算法问题,又有实际应用意义
通过掌握股票买卖问题的解法,我们不仅学会了具体的算法技巧,更重要的是理解了如何将现实问题抽象为状态机,如何设计状态和转移方程。
系列文章回顾
推荐练习
- 在LeetCode上练习股票买卖相关的所有题目
- 尝试实现通用的股票买卖问题求解框架
- 思考其他具有状态机特征的动态规划问题