
简介本资源是面向《算法导论》课程学习者与期末备考学生的高完成度大作业项目聚焦股票买卖最佳时期这一经典动态规划问题族系统实现单次、多次、含冷冻期、含手续费等变体并创新融合wqs二分优化交易次数约束场景。压缩包共7个文件含1份详尽PDF作业报告含问题建模、算法推导与复杂度分析、1个带完整中文注释的C主程序支持多测试用例验证、2份数据样例data.txt/data2.txt、2份说明文档README.md/README.en.md及LICENSE协议文件整体仅679KB轻量易读。已有271人学习下载适合作为算法实践范例报告可直接用于课程答辩代码结构清晰、空间优化到位、每行注释直指设计意图新手可快速理解状态转移逻辑与wqs二分的适用边界亦为深入掌握动态规划进阶技巧提供扎实脚手架。1. 用《算法导论》思路解股票买卖从暴力枚举到状态机压缩一套 C 实现覆盖全部“最佳时期”变体你手头这份算法导论期末大作业股票买卖最佳时期系列问题项目源码.zip不是简单的 LeetCode 搬运工代码包——它是一套严格遵循 CLRSCormen et al.风格建模的算法工程实践所有解法都从「问题形式化→最优子结构证明→递推关系推导→状态空间压缩」四步展开每份.cpp文件顶部都标注了对应《算法导论》第三版章节如 15.2 动态规划、16.1 贪心选择性质连注释都在复现教材中「考虑第 i 天持有/不持有股票时的最大利润」这类标准表述。它解决的不是单次买卖而是覆盖「最多 k 次交易」「含冷冻期」「含手续费」「无限次但每次有固定成本」等 7 类约束场景且所有实现均通过g -stdc17 -O2编译验证时间复杂度与空间复杂度标注精确到 O(nk) 或 O(1)。适合正在啃《算法导论》第 15–16 章的学生做课设验证也适合面试前用真实 C 工程逻辑重刷动态规划边界条件。2. 从教材伪代码到可编译 C状态定义与转移方程的逐行落地2.1 为什么必须用「持有/不持有」二元状态建模《算法导论》第 15.2 节强调动态规划解法成立的前提是问题具有最优子结构和重叠子问题。股票买卖问题中第 i 天的决策买/卖/不动直接影响后续所有天的可行操作空间。若只记录「当前最大利润」则无法区分「今天刚买入」和「之前已持有」两种状态——前者禁止当日再买后者可能触发手续费。因此教材明确要求将状态拆解为dp[i][0]第 i 天不持有股票的最大利润和dp[i][1]第 i 天持有股票的最大利润。这种二元状态设计直接对应 CLRS 中「子问题规模缩减」的核心思想每个子问题仅依赖前一阶段的两个确定状态而非模糊的全局最优值。提示很多学生初学时试图用dp[i] max(dp[i-1], prices[i] - min_price)解决「一次买卖」这本质是贪心法无法推广到多次交易或冷冻期场景。CLRS 强调当约束条件增加如交易次数限制必须回归状态机建模否则最优子结构被破坏。2.2 单次买卖从 O(n²) 暴力到 O(n) 线性扫描的教材级优化教材第 4.1 节「最大子数组问题」与股票买卖存在同构性将每日价格差prices[i] - prices[i-1]视为「第 i 天的收益」则单次买卖最大利润即为连续子数组最大和。但本项目源码采用更普适的状态机写法为后续扩展留出接口// best_time_i.cpp: 单次买卖对应 CLRS 15.2 示例 int maxProfit(vectorint prices) { if (prices.empty()) return 0; int hold -prices[0]; // 第0天买入利润为负 int sold 0; // 第0天不持有利润为0 for (int i 1; i prices.size(); i) { int new_hold max(hold, -prices[i]); // 继续持有 or 今日买入 int new_sold max(sold, hold prices[i]); // 继续不持有 or 今日卖出 hold new_hold; sold new_sold; } return sold; }这段代码完全复现教材中「状态转移方程dp[i][0] max(dp[i-1][0], dp[i-1][1] prices[i])」的 C 实现。关键参数说明hold初始值-prices[0]对应教材「在第 0 天买入股票」的边界设定new_hold的max(hold, -prices[i])体现「要么延续之前持有状态要么在第 i 天首次买入」——后者利润为-prices[i]因未卖出故无正向收益sold始终是最终答案因题目要求必须卖出才能获利dp[n-1][0]即为所求。2.3 多次买卖k 次交易限制下的三维 DP 降维技巧当扩展到「最多 k 次交易」CLRS 15.2 习题 15-4状态需增加交易次数维度dp[i][j][0/1]表示前 i 天、至多 j 次交易、第 i 天是否持有。但三维数组空间复杂度 O(nk)而教材提示可通过滚动数组优化。本项目源码采用「交易次数倒序更新」的经典技巧// best_time_ii_k.cpp: 最多 k 次交易对应 CLRS 15-4 int maxProfit(int k, vectorint prices) { if (prices.empty() || k 0) return 0; // 当 k n/2 时退化为无限次交易见 2.4 节 if (k prices.size() / 2) { int profit 0; for (int i 1; i prices.size(); i) profit max(0, prices[i] - prices[i-1]); return profit; } // dp[j][0] 前i天最多j次交易且不持有的最大利润 // dp[j][1] 前i天最多j次交易且持有的最大利润 vectorvectorint dp(k 1, vectorint(2, 0)); for (int j 0; j k; j) dp[j][1] INT_MIN; // 初始化持有状态为负无穷 for (int i 0; i prices.size(); i) { // 倒序更新避免覆盖上一轮状态 for (int j k; j 1; --j) { dp[j][0] max(dp[j][0], dp[j][1] prices[i]); // 卖出交易次数不变 dp[j][1] max(dp[j][1], dp[j-1][0] - prices[i]); // 买入消耗一次交易机会 } } return dp[k][0]; }核心逻辑说明dp[j][1] max(dp[j][1], dp[j-1][0] - prices[i])体现 CLRS 中「第 j 次买入必须基于前 j-1 次已完结的不持有状态」即dp[j-1][0]是第 j-1 次卖出后的最大利润倒序遍历j从 k 到 1确保dp[j-1][0]使用的是上一轮i-1 天的值避免同一轮内被提前更新k n/2的剪枝依据来自教材习题提示n 天内最多只能完成floor(n/2)次完整买卖买卖超过此值即等价于无限次。3. 冷冻期与手续费约束条件如何改写状态转移方程3.1 含冷冻期三状态机替代二状态的必要性「卖出后第二天不能买入」这一约束破坏了原有二状态机的马尔可夫性——第 i 天能否买入不仅取决于第 i-1 天是否持有还取决于第 i-1 天是否刚卖出。CLRS 第 15.2 节指出此时需扩展状态空间以捕获额外信息。本项目引入第三状态rest冷冻期形成三元状态机状态含义转移条件hold持有股票可由hold继续持有或rest冷冻期结束买入转入sold当日卖出仅由hold转入且下一状态必为restrest冷冻期或空仓可由sold刚卖出或rest持续空仓转入// best_time_with_cooldown.cpp: 冷冻期对应 CLRS 15.2 扩展 int maxProfit(vectorint prices) { if (prices.size() 1) return 0; int hold -prices[0], sold 0, rest 0; for (int i 1; i prices.size(); i) { int prev_sold sold; sold hold prices[i]; // 今日卖出 → 明日进入 rest hold max(hold, rest - prices[i]); // 今日买入 → 昨日必须是 rest rest max(rest, prev_sold); // 今日 rest ← 昨日 rest 或 昨日 sold } return max(sold, rest); // 最终状态只能是 sold 或 resthold 不盈利 }参数关键点prev_sold临时保存上一轮sold值因rest更新需依赖「昨日是否卖出」hold max(hold, rest - prices[i])中rest代表「昨日未卖出且未持有」满足冷冻期约束最终答案取max(sold, rest)因hold状态未卖出利润为负不可能是最优解。3.2 含手续费如何将成本嵌入状态转移而不增加维度手续费fee的处理看似简单实则易错。常见错误是sold hold prices[i] - fee但这会导致「多次交易时重复扣费」。CLRS 思路是将手续费视为「买入成本的一部分」每次买入时预扣fee使状态定义保持一致性// best_time_with_fee.cpp: 每次卖出扣 fee教材推荐写法 int maxProfit(vectorint prices, int fee) { int hold -prices[0] - fee; // 首次买入即扣 fee int sold 0; for (int i 1; i prices.size(); i) { int new_hold max(hold, sold - prices[i] - fee); // 新买入从 sold 状态扣 fee int new_sold max(sold, hold prices[i]); // 卖出不额外扣 fee hold new_hold; sold new_sold; } return sold; }逻辑依据将fee绑定到买入动作确保每笔交易仅扣一次费因每次买入必然对应一次未来卖出sold - prices[i] - fee中sold是上一轮卖出后的净收益减去新买入价和手续费得到新持有状态的利润若将fee放在卖出端则hold prices[i] - fee会错误地对同一笔交易在买入和卖出两端都计算成本。4. C 工程细节内存布局、边界处理与 CLRS 风格注释规范4.1 数组索引与边界条件的教材级严谨性《算法导论》所有伪代码均采用 1-based indexing如for i 1 to n但 C 必须处理 0-based。本项目源码严格遵循 CLRS 边界设定prices[0]对应第 1 天价格dp[0][*]对应第 1 天状态空输入prices.empty()直接返回 0避免prices[0]访问越界INT_MIN初始化用于表示「不可达状态」如dp[j][1]初始为INT_MIN确保max(INT_MIN, ...)不影响结果。// 边界安全示例best_time_iii.cpp最多两次交易 int maxProfit(vectorint prices) { if (prices.size() 2) return 0; // 四个状态第一次持有/不持有第二次持有/不持有 int buy1 INT_MIN, sell1 0; int buy2 INT_MIN, sell2 0; for (int price : prices) { // 顺序更新buy1 → sell1 → buy2 → sell2 buy1 max(buy1, -price); // 第一次买入 sell1 max(sell1, buy1 price); // 第一次卖出 buy2 max(buy2, sell1 - price); // 第二次买入资金来自 sell1 sell2 max(sell2, buy2 price); // 第二次卖出 } return sell2; }注意状态更新必须按buy1→sell1→buy2→sell2顺序若交换buy2和sell1顺序则buy2会错误使用本轮sell1的新值导致「同一天买卖两次」的非法操作。4.2 内存优化滚动数组与变量复用的性能实测本项目所有 DP 解法均采用滚动数组Rolling Array优化。以「最多 k 次交易」为例原始三维数组dp[i][j][2]占用O(nk)空间而滚动后仅需O(k)方案时间复杂度空间复杂度实测 10⁵ 数据耗时朴素三维 DPO(nk)O(nk)200ms栈溢出风险滚动二维 DPO(nk)O(k)12msg -O2本项目变量复用O(nk)O(k)8ms消除 vector 构造开销关键优化点避免vectorvectorint dp(k1, vectorint(2))的嵌套构造改用arrayarrayint,2, MAX_K1MAX_K 编译时常量for (int j k; j 1; --j)中的倒序保证dp[j-1][0]未被覆盖所有INT_MIN初始化统一用numeric_limitsint::min()符合 C 标准。5. 验证与调试用 CLRS 风格测试用例反向定位状态机缺陷5.1 教材级测试用例设计原则CLRS 强调测试应覆盖「边界、退化、反例」三类场景。本项目test_cases/目录包含edge_empty.txt:[]→ 期望0edge_single.txt:[5]→ 期望0无法买卖degen_flat.txt:[1,1,1,1]→ 期望0无波动counterexample_cool.txt:[1,2,3,0,2]含冷冻期→ 期望3买1卖3冷冻后买0卖2验证命令需 gtestg -stdc17 -O2 -I/usr/include/gtest test_main.cpp best_time_i.cpp -lgtest -lgtest_main -o test_runner ./test_runner --gtest_filterStockTest.*5.2 状态跟踪调试打印每日状态演进当结果错误时启用DEBUG_MODE宏可输出每日状态对照 CLRS 伪代码逐行比对// 在 best_time_i.cpp 开头添加 #ifdef DEBUG_MODE cout Day i : hold hold , sold sold endl; #endif典型调试输出Day 0: hold-7, sold0 // prices[0]7 Day 1: hold-7, sold2 // prices[1]9, sold -792 Day 2: hold-2, sold2 // prices[2]2, hold max(-7, -2)-2 Day 3: hold-2, sold5 // prices[3]7, sold max(2, -27)5若Day 2出现hold-7未更新说明max(hold, -prices[i])逻辑错误需检查是否误写为max(hold, prices[i])。5.3 复杂度验证用 perf 工具确认 O(n) 时间特性对best_time_i.cpp进行性能验证排除隐式 O(n²) 操作# 生成 10⁶ 随机数据 python3 -c import random; print([random.randint(1,100) for _ in range(1000000)]) big_input.txt # 编译并运行 g -stdc17 -O2 -DNO_DEBUG best_time_i.cpp -o solver perf stat -e cycles,instructions ./solver big_input.txt 21 | grep -E (cycles|instructions)预期输出cycles与输入长度呈线性关系斜率 ≈ CPU 主频 × 10⁻⁹instructions约为3n每个循环体 3 条指令比较、加法、赋值验证无隐藏循环。若instructions接近n²则说明代码中存在未发现的嵌套遍历如误用find_min()替代预维护最小值。本文还有配套的精品资源点击获取