1. 股票问题与算法选择
股票买卖问题是算法学习中非常经典的案例,它能很好地帮助我们理解动态规划和贪心算法的核心思想。这类问题通常给定一段时间内股票每天的价格,要求计算在不同买卖规则下的最大收益。
在实际操作中,我发现很多初学者容易混淆动态规划和贪心算法的应用场景。动态规划适合解决具有最优子结构的问题,而贪心算法则适用于局部最优能导致全局最优的情况。对于股票问题,我们需要根据具体约束条件来选择合适的方法。
2. 基础问题解析
2.1 单次买卖问题(121题)
这是最简单的股票问题:只允许完成一次交易(买入和卖出各一次),求最大利润。
cpp复制int maxProfit(vector<int>& prices) {
int min_price = INT_MAX;
int max_profit = 0;
for(int price : prices){
min_price = min(min_price, price);
max_profit = max(max_profit, price - min_price);
}
return max_profit;
}
这个解法使用了贪心思想,维护一个历史最低价和当前最大利润。时间复杂度O(n),空间复杂度O(1)。
注意:这里min_price初始化为INT_MAX是为了确保第一次比较时能正确更新。实际编码中要特别注意这种边界条件。
2.2 多次买卖问题(122题)
允许进行多次交易,但必须在再次购买前出售掉之前的股票。
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;
}
这个解法巧妙地利用了贪心算法,只要今天的价格比昨天高,就假设昨天买入今天卖出。虽然看起来进行了很多次交易,但实际上可以合并为连续的买卖操作。
3. 动态规划解法进阶
3.1 含冷冻期问题(309题)
卖出股票后需要等待一天才能再次买入。这类问题就需要使用动态规划来解决了。
cpp复制int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n <= 1) return 0;
vector<int> hold(n), sold(n), rest(n);
hold[0] = -prices[0];
sold[0] = 0;
rest[0] = 0;
for(int i = 1; i < n; ++i){
hold[i] = max(hold[i-1], rest[i-1] - prices[i]);
sold[i] = hold[i-1] + prices[i];
rest[i] = max(rest[i-1], sold[i-1]);
}
return max(sold[n-1], rest[n-1]);
}
这里定义了三个状态:
- hold[i]:第i天持有股票时的最大收益
- sold[i]:第i天卖出股票时的最大收益
- rest[i]:第i天不持有股票且不操作时的最大收益
3.2 含手续费问题(714题)
每次交易需要支付固定手续费。
cpp复制int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
int cash = 0, hold = -prices[0];
for(int i = 1; i < n; ++i){
cash = max(cash, hold + prices[i] - fee);
hold = max(hold, cash - prices[i]);
}
return cash;
}
这个解法优化了空间复杂度,只维护两个状态变量。每次交易时扣除手续费即可。
4. 通用动态规划框架
对于更复杂的股票问题,我们可以建立一个通用的DP框架:
cpp复制dp[i][k][0 or 1]
// i: 第i天
// k: 剩余交易次数
// 0: 不持有股票
// 1: 持有股票
// 初始状态
dp[0][k][0] = 0;
dp[0][k][1] = -prices[0];
// 状态转移方程
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]);
这个框架可以解决大多数股票买卖问题,只需要根据具体条件调整k的定义和状态转移方程。
5. 实战技巧与常见错误
5.1 边界条件处理
在实际编码中,我发现边界条件是最容易出错的地方。比如:
- 空输入或单元素输入
- 所有价格递减的情况
- 手续费为0的特殊情况
建议在函数开头先处理这些特殊情况。
5.2 空间优化技巧
很多股票问题的DP解法都可以进行空间优化,从O(n)降到O(1)。关键是要理清楚状态转移只依赖于前一个状态。
5.3 调试方法
当DP解法出现问题时,可以:
- 打印整个DP表
- 检查初始状态是否正确
- 验证状态转移方程是否与问题描述一致
6. 复杂度分析
对于大多数股票问题:
- 时间复杂度:O(n)
- 空间复杂度:基础解法O(n),优化后O(1)
当涉及交易次数限制k时,复杂度可能变为O(nk)。在实际面试中,需要根据问题约束选择合适的解法。
7. 扩展思考
股票问题虽然看似简单,但它涵盖了算法设计的多个重要概念:
- 如何定义合适的状态
- 如何建立状态转移方程
- 如何进行空间优化
- 如何选择贪心还是DP
掌握这类问题的解法,对提升算法思维能力大有裨益。我建议初学者可以从最简单的版本开始,逐步增加约束条件,体会不同解法之间的区别和联系。