股票买卖问题是算法学习中一个经典的实际应用场景。题目给定一个长度为n的数组prices,其中prices[i]表示第i天的股票价格。我们可以在任意天数进行多次买卖操作,但任何时候最多只能持有一股股票(即买入前必须卖出当前持有的股票)。目标是计算出能够获得的最大利润。
这个问题有几个关键约束条件需要注意:
理解这些约束条件非常重要,因为它们直接影响我们解决问题的思路和算法设计。例如,由于交易次数不受限制,我们可以在价格上升的任何时候进行买卖操作;而持股限制则意味着我们不需要考虑复杂的仓位管理问题。
贪心算法是解决这个问题的直观方法。其核心思想可以概括为"低买高卖"——只要第二天的价格比当天高,就在当天买入并在第二天卖出。这样可以将整个价格序列分解为多个小的盈利区间,累加这些小的盈利就能得到最大总利润。
这种方法的有效性基于一个关键观察:对于任何连续上涨的区间,多次买卖的累计利润等于最高价与最低价的差。例如,对于价格序列[1,2,3,4],1买4卖的利润是3,而1买2卖+2买3卖+3买4卖的利润也是(2-1)+(3-2)+(4-3)=3。
这种方法的优势在于:
cpp复制int maxProfit(vector<int>& prices) {
int profit = 0;
for (int i = 1; i < prices.size(); ++i) {
if (prices[i] > prices[i-1]) {
profit += prices[i] - prices[i-1];
}
}
return profit;
}
贪心算法的正确性可以通过数学归纳法证明。对于任意长度的价格序列,只要存在价格上升的相邻天数,将这些局部利润相加就能得到全局最优解。这是因为题目允许无限次交易,所以可以将整个价格序列分解为多个单调递增的子序列。
注意:贪心算法适用于这个特定问题,是因为交易次数不受限制。如果对交易次数有限制(如最多只能进行k次交易),贪心算法就不再适用,需要考虑动态规划等其他方法。
虽然贪心算法更简单高效,但用动态规划解决这个问题有助于我们理解更复杂的股票交易问题。动态规划的思路是定义状态并找到状态转移方程。
对于每一天,我们有两种状态:
定义dp[i][j]表示第i天处于状态j时的最大利润。我们的目标是求dp[n-1][0](最后一天不持有股票时的最大利润)。
对于第i天:
如果不持有股票(dp[i][0]),可能有两种情况:
如果持有股票(dp[i][1]),可能有两种情况:
初始条件需要考虑第一天的情况:
cpp复制int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2) return 0;
vector<vector<int>> dp(n, vector<int>(2));
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < n; ++i) {
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];
}
观察状态转移方程可以发现,第i天的状态只依赖于第i-1天的状态,因此可以将空间复杂度从O(n)优化到O(1):
cpp复制int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2) return 0;
int dp0 = 0; // 不持有股票
int dp1 = -prices[0]; // 持有股票
for (int i = 1; i < n; ++i) {
int new_dp0 = max(dp0, dp1 + prices[i]);
int new_dp1 = max(dp1, dp0 - prices[i]);
dp0 = new_dp0;
dp1 = new_dp1;
}
return dp0;
}
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 贪心算法 | O(n) | O(1) |
| 动态规划 | O(n) | O(n) |
| 优化动态规划 | O(n) | O(1) |
贪心算法:
动态规划:
对于这个特定问题,贪心算法显然是更好的选择,因为它更简单高效。但是学习动态规划解法仍然很有价值,因为:
在实际编码中,有几个边界条件需要特别注意:
如果每次交易需要支付固定费用fee,如何修改算法?
贪心算法解法需要调整:
cpp复制int maxProfit(vector<int>& prices, int fee) {
int profit = 0;
int minPrice = prices[0];
for (int i = 1; i < prices.size(); ++i) {
if (prices[i] < minPrice) {
minPrice = prices[i];
} else if (prices[i] > minPrice + fee) {
profit += prices[i] - minPrice - fee;
minPrice = prices[i] - fee; // 关键调整
}
}
return profit;
}
如果卖出后需要等待一天才能再次买入,如何修改动态规划解法?
需要引入第三个状态表示冷却期:
cpp复制int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2) return 0;
vector<vector<int>> dp(n, vector<int>(3));
dp[0][0] = 0; // 不持有,非冷却
dp[0][1] = -prices[0]; // 持有
dp[0][2] = 0; // 不持有,冷却
for (int i = 1; i < n; ++i) {
dp[i][0] = max(dp[i-1][0], dp[i-1][2]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
dp[i][2] = dp[i-1][1] + prices[i];
}
return max(dp[n-1][0], dp[n-1][2]);
}
对于最多允许k次交易的通用情况,动态规划解法需要扩展状态:
cpp复制int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if (n < 2 || k == 0) return 0;
if (k >= n/2) {
// 等同于无限次交易,使用贪心算法
int profit = 0;
for (int i = 1; i < n; ++i) {
if (prices[i] > prices[i-1]) {
profit += prices[i] - prices[i-1];
}
}
return profit;
}
vector<vector<int>> dp(k+1, vector<int>(n));
for (int t = 1; t <= k; ++t) {
int maxDiff = -prices[0];
for (int i = 1; i < n; ++i) {
dp[t][i] = max(dp[t][i-1], prices[i] + maxDiff);
maxDiff = max(maxDiff, dp[t-1][i] - prices[i]);
}
}
return dp[k][n-1];
}
股票买卖算法在实际中有多种应用:
掌握这些问题的解法,就能应对大多数股票买卖相关的算法问题了。关键在于理解状态定义和转移方程的推导过程,而不是死记硬背代码。