资讯详情

资讯详情

动态规划解决含冷冻期的股票买卖问题

1. 问题背景与核心挑战股票买卖问题是算法领域的经典动态规划题目而含冷冻期的变种在实际投资场景中尤为常见。想象一下你是一名短线交易员每天需要决定是否买卖某支股票。但交易所规定卖出股票后必须隔一天才能再次买入。这种限制条件就是我们所说的冷冻期。这个约束看似简单却彻底改变了问题的状态转移逻辑。常规的无限次交易问题只需要考虑当天是否持有股票而冷冻期规则引入了时间维度的依赖关系。我们需要设计一个算法在O(n)时间复杂度内计算出最大利润同时严格遵守交易规则。2. 算法设计思路解析2.1 状态定义与记忆化设计我们采用自上而下的记忆化递归方法定义两个核心状态持有股票is_hold true不持有股票is_hold false记忆化数组memo是一个二维数组memo[i][0]表示第i天不持有股票时的最大利润memo[i][1]表示第i天持有股票时的最大利润。这种设计将时间复杂度从指数级O(2^n)降到了线性O(n)。关键技巧记忆化数组的初始化值为-1用于标记该状态尚未计算。这种设计比使用特殊值表示未计算状态更安全可靠。2.2 递归边界条件处理递归的终止条件需要谨慎处理if(end 0) { if(is_hold) return -0xFFFF; // 非法状态没有天数却持有股票 return 0; // 合法状态没有天数也不持有股票 }这里使用-0xFFFF表示非法状态比INT_MIN更安全避免了可能的整数溢出问题。2.3 状态转移方程2.3.1 持有股票的状态转移当第end天持有股票时有两种可能前一天已经持有今天不操作今天新买入需考虑冷冻期res max( dfs(end-1, true, prices), // 情况1继续持有 dfs(end-2, false, prices) - prices[end] // 情况2买入操作 );特别注意买入操作的前驱状态是end-2天这确保了卖出后至少隔一天才能买入完美实现了冷冻期限制。2.3.2 不持有股票的状态转移当第end天不持有股票时也有两种可能前一天已经不持有今天不操作今天卖出持有的股票res max( dfs(end-1, false, prices), // 情况1继续不持有 dfs(end-1, true, prices) prices[end] // 情况2卖出操作 );卖出操作不受冷冻期限制因此前驱状态仍然是end-1天。3. 完整代码实现与解析class Solution { public: vectorvectorint memo; // 记忆化数组 int dfs(int end, bool is_hold, vectorint prices) { if(end 0) { return is_hold ? -0xFFFF : 0; } int state is_hold ? 1 : 0; if(memo[end][state] ! -1) { return memo[end][state]; } int res; if(is_hold) { res max( dfs(end-1, true, prices), (end 1 ? dfs(end-2, false, prices) : 0) - prices[end] ); } else { res max( dfs(end-1, false, prices), dfs(end-1, true, prices) prices[end] ); } return memo[end][state] res; } int maxProfit(vectorint prices) { int n prices.size(); if(n 1) return 0; memo.assign(n, vectorint(2, -1)); return dfs(n-1, false, prices); } };3.1 边界条件处理增强在实现中我们对买入操作做了更严格的边界检查(end 1 ? dfs(end-2, false, prices) : 0) - prices[end]这确保了当end0时即第一天尝试买入不会出现数组越界访问。3.2 空间复杂度优化虽然我们使用了O(n)的额外空间存储记忆化数组但在实际应用中可以通过滚动数组技术将空间复杂度优化到O(1)。不过对于算法题而言O(n)的空间通常已经足够。4. 算法正确性验证4.1 示例测试以题目给出的示例1进行验证 输入: prices [1,2,3,0,2] 预期输出: 3算法执行过程第0天买入价格1第1天卖出价格2利润1第2天冷冻期第3天买入价格0第4天卖出价格2利润2 总利润1 2 3与预期一致。4.2 极端情况测试测试空数组 输入: prices [] 输出: 0测试单元素数组 输入: prices [1] 输出: 0测试递减序列 输入: prices [5,4,3,2,1] 输出: 0无法获利测试递增序列 输入: prices [1,2,3,4,5] 输出: 41买5卖5. 性能分析与优化5.1 时间复杂度分析由于记忆化数组的存在每个状态最多计算一次。对于n天的价格序列共有2n个状态每天有持有/不持有两种状态因此时间复杂度为O(n)。5.2 空间复杂度分析记忆化数组需要O(n)空间递归栈深度在最坏情况下也是O(n)因此总空间复杂度为O(n)。5.3 实际运行优化在实际编码竞赛中可以考虑以下优化将递归改为迭代避免递归栈开销使用滚动数组减少空间使用对于C使用全局变量而非类成员变量可能稍快6. 常见问题与调试技巧6.1 为什么买入操作要查看end-2的状态这是冷冻期规则的核心体现。卖出股票后第二天不能买入因此买入操作必须保证前一天没有卖出操作。通过查看end-2的状态我们确保了在end-1天没有卖出操作。6.2 如何处理第一天和第二天买入的情况对于第一天end0可以买入没有前一天的限制但不能卖出因为没有持有股票对于第二天end1如果第一天买入了可以卖出如果要买入需要查看end-2-1天的状态即初始状态6.3 记忆化数组初始值为什么是-1-1在这里表示该状态尚未计算。使用-1而不是0是因为0可能是某些状态的合法值如不持有股票时的利润。如果使用0作为初始值就无法区分未计算和计算后结果为0两种情况。6.4 如何验证状态转移的正确性可以手动模拟小规模输入如prices [1,2,3]。记录每个memo[i][j]的值检查是否符合预期。特别是注意买入操作是否真的隔了一天利润计算是否正确累加7. 算法变种与扩展思考7.1 含交易费用的变种如果每次交易需要支付固定费用fee只需修改状态转移方程// 卖出时扣除费用 res max( dfs(end-1, false, prices), dfs(end-1, true, prices) prices[end] - fee );7.2 限制交易次数的变种如果需要限制总交易次数k次状态需要增加一个维度记录已完成的交易次数。这会增加时间复杂度到O(nk)。7.3 多支股票的扩展如果有多支股票可以交易状态需要增加股票种类的维度。复杂度会随股票数量线性增长。8. 实际应用中的注意事项价格序列预处理实际股票数据可能有缺失值需要先进行清洗整数溢出问题对于极端价格序列利润可能超过INT_MAX浮点数精度如果价格是浮点数比较操作需要设置epsilon实时性要求高频交易场景可能需要更高效的实现9. 从递归到动态规划的转换虽然记忆化递归直观易懂但在实际工程中迭代式的动态规划通常更高效。我们可以将上述算法转换为自底向上的DPint maxProfit(vectorint prices) { int n prices.size(); if(n 1) return 0; vectorvectorint dp(n, vectorint(2)); dp[0][0] 0; dp[0][1] -prices[0]; dp[1][0] max(dp[0][0], dp[0][1] prices[1]); dp[1][1] max(dp[0][1], -prices[1]); for(int i 2; 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-2][0] - prices[i]); } return dp[n-1][0]; }这种实现完全避免了递归开销运行速度更快也更适合大规模数据。10. 算法选择建议对于不同场景可以考虑以下选择编程竞赛记忆化递归更易编写和调试工程实现迭代式DP更高效稳定教学讲解记忆化递归更直观易懂超大数据量考虑空间优化的DP变种在实际面试中建议先实现记忆化递归版本再讨论如何优化为DP展示完整的思考过程。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →