股票买卖问题是算法面试和竞赛中的经典题型,也是动态规划应用的绝佳场景。这类问题的核心在于如何在价格波动中通过买卖操作获取最大利润,同时遵守各种交易限制条件。动态规划之所以能高效解决这类问题,是因为它能够将复杂问题分解为多个重叠子问题,并通过状态转移方程系统地记录和更新最优解。
在实际操作中,我发现这类问题通常需要考虑三个关键要素:
理解这些约束条件对状态设计的影响,是解决此类问题的关键所在。下面我将结合三个典型题目,详细解析如何设计状态和状态转移方程。
这个问题是股票买卖系列中最具一般性的情况之一,它限制了最多可以进行K次交易。每次交易包含买入和卖出两个操作,因此我们需要设计能够记录交易次数的状态。
我采用的解决方案是使用一个二维DP数组:
这种设计巧妙地用数组索引的奇偶性来区分持有/不持有状态,同时通过索引值记录已完成的交易次数。例如:
初始化阶段,我们需要设置第一天各种可能状态的初始值:
cpp复制for(int j=1;j<=k;j++) {
dp[0][2*j-1] = -prices[0]; // 第j次交易持有股票的初始状态
}
状态转移方程分为两种情况:
cpp复制dp[i][2*j-1] = max(
dp[i-1][2*j-2] - prices[i], // 前一天不持有,今天买入
dp[i-1][2*j-1] // 保持持有状态
);
cpp复制dp[i][2*j] = max(
dp[i-1][2*j], // 保持不持有状态
dp[i-1][2*j-1] + prices[i] // 前一天持有,今天卖出
);
在实际编码中,有几个关键点需要注意:
提示:当K值很大时(K ≥ n/2),实际上相当于没有交易次数限制,这种情况可以直接采用贪心算法计算所有上升区间的利润和,能显著提高效率。
冷冻期问题引入了新的约束:卖出股票后需要等待一天才能再次买入。这使得传统的两种状态(持有/不持有)不足以描述所有情况。
经过多次尝试,我发现需要四种状态才能准确描述:
这种精细的状态划分是解决问题的关键,它能够准确反映冷冻期对交易决策的影响。
每种状态的转移关系如下:
cpp复制dp[i][0] = max(
dp[i-1][0], // 保持持有
dp[i-1][1] - prices[i], // 从非冷冻期买入
dp[i-1][3] - prices[i] // 从冷冻期结束后买入
);
cpp复制dp[i][1] = max(
dp[i-1][1], // 继续保持
dp[i-1][3] // 冷冻期结束
);
cpp复制dp[i][2] = dp[i-1][0] + prices[i]; // 必须从持有状态卖出
cpp复制dp[i][3] = dp[i-1][2]; // 前一天卖出才会进入冷冻期
在实现这个算法时,有几个容易出错的地方:
注意:冷冻期问题的状态设计是这类问题的核心难点。如果状态划分不够细致,很容易遗漏某些转移可能性,导致计算结果不正确。
这个问题在无限次交易的基础上增加了手续费成本。手续费可以在买入或卖出时扣除,两种方式在实现上略有不同。
我选择在买入时扣除手续费的方式,这样状态转移更为直观:
cpp复制dp[i][0] = max(
dp[i-1][0], // 保持持有
dp[i-1][1] - prices[i] - fee // 买入并支付手续费
);
cpp复制dp[i][1] = max(
dp[i-1][1], // 保持不持有
dp[i-1][0] + prices[i] // 卖出股票
);
在实际操作中,手续费的处理方式会影响代码实现:
两种方式在数学上是等价的,但选择一种并保持一致很重要。此外,还需要注意:
通过这三个问题的解决,我总结出了一套设计股票买卖问题状态的方法:
构建状态转移方程时,可以采用以下步骤:
原始的二维DP数组会消耗O(nk)的空间,在实际应用中可以进行优化:
例如,714题的空间优化版本:
cpp复制int hold = -prices[0]-fee;
int notHold = 0;
for(int i=1; i<prices.size(); i++) {
int preHold = hold;
hold = max(hold, notHold - prices[i] - fee);
notHold = max(notHold, preHold + prices[i]);
}
return notHold;
最常见的错误是初始状态设置不正确。例如:
调试方法:打印出DP数组的前几行,检查初始值是否符合预期。
另一个常见问题是遗漏某些状态转移可能性。例如:
调试方法:针对每个状态,手动验证所有可能的转移路径。
特殊边界条件容易出错:
调试方法:单独编写测试用例覆盖这些边界情况。
这三种解法的时间复杂度均为O(nk),其中:
原始空间复杂度为O(nk),通过以下方法优化:
在实际测试中,我发现:
股票买卖问题还有许多其他变种:
对于更复杂的问题,可能需要:
这类算法不仅用于面试,在实际金融领域也有应用:
经过多次实践,我认为掌握这类问题的关键在于深入理解状态设计和转移方程的构建原理。每次遇到新的约束条件,都应该先思考它对状态空间的影响,再设计相应的状态转移方式。这种思维方式不仅适用于股票买卖问题,也能推广到其他动态规划问题的求解中。