【计算机算法设计与分析】动态规划与贪心算法教程:从矩阵连乘到资源优化
文章目录
- 一、算法设计范式概览
- 动态规划 vs 贪心算法
- 二、问题一:矩阵连乘问题(动态规划经典案例)
- 问题描述
- 为什么用动态规划?
- 动态规划算法设计
- 1. 状态定义
- 2. 状态转移方程
- 3. 填表过程(自底而上)
- 4. 结果与回溯
- 5. 复杂度分析
- 关键洞察
- 三、问题二:钢条切割问题(动态规划简化版)
- 问题描述
- 问题特性
- 动态规划算法设计
- 1. 状态定义
- 2. 状态转移方程
- 3. 填表过程(n=6)
- 4. 结果
- 5. 复杂度分析
- 与矩阵连乘的对比
- 四、问题三:苹果买卖问题(贪心算法案例)
- 问题描述
- 为什么用贪心算法?
- 贪心算法设计
- 1. 核心思路
- 2. 算法伪代码
- 3. 算法执行示例
- 4. 正确性证明
- 5. 复杂度分析
- 与动态规划的对比
- 五、三种问题的算法选择决策树
- 问题特征对比表
- 六、总结:算法选择的决策框架
- 核心原则
- 性能对比
本文通过三个经典问题——矩阵连乘、钢条切割、苹果买卖,深入解析动态规划和贪心算法的核心思想、设计逻辑和应用场景,帮助读者掌握这两种重要的算法设计范式。
一、算法设计范式概览
动态规划 vs 贪心算法
共同点:
- 都用于解决优化问题
- 都利用最优子结构特性:原问题的最优解由子问题的最优解组成
- 都需要将大问题分解为子问题
核心区别:
- 动态规划:需要枚举所有可能的分解情况,通过"查表"避免重复计算,通常需要解决所有子问题
- 贪心算法:通过"贪心选择特性",每一步只做局部最优决策,不需要枚举所有可能
选择原则:
- 如果问题满足贪心选择特性(局部最优能保证全局最优),优先用贪心算法(通常更简单高效)
- 如果只满足最优子结构但不满足贪心选择特性,用动态规划
二、问题一:矩阵连乘问题(动态规划经典案例)
问题描述
给定5个矩阵A、B、C、D、E,维度分别为:
- A: 6×11
- B: 11×7
- C: 7×15
- D: 15×3
- E: 3×21
目标:找到最优连乘顺序,使总乘法次数最少。
为什么用动态规划?
问题特性分析:
- 最优子结构:如果
A₁...Aₙ的最优顺序是(A₁...Aₖ)(Aₖ₊₁...Aₙ),那么两个子序列也必须是各自的最优顺序 - 重叠子问题:
(A₂A₃A₄)既是(A₂A₃A₄A₅)的子问题,也是(A₁A₂A₃A₄)的子问题 - 不满足贪心选择:无法通过"局部最优"直接确定全局最优,必须枚举所有切割点
穷举法不可行:
- n个矩阵的加括号方式数量是卡塔兰数,指数级增长
- n=5时有14种方式,n=10时有4862种,n=20时超过10⁹种
动态规划算法设计
1. 状态定义
OPT(i, j) = 计算矩阵链 AᵢAᵢ₊₁...Aⱼ 所需的最少乘法次数
2. 状态转移方程
OPT(i, j) = min{OPT(i, k) + OPT(k+1, j) + pᵢ₋₁pₖpⱼ} (i ≤ k ≤ j-1)
逻辑解释:
- 任何连乘顺序都可以看作"先连乘左边,再连乘右边,最后合并"
- 枚举所有可能的切割点k,选择总代价最小的
pᵢ₋₁pₖpⱼ是合并两个子矩阵的代价
3. 填表过程(自底而上)
维度序列:p = [6, 11, 7, 15, 3, 21]
步骤1:初始化
OPT[i, i] = 0(单个矩阵不需要乘法)
步骤2:长度为2的链
OPT[1,2] = 6×11×7 = 462OPT[2,3] = 11×7×15 = 1155OPT[3,4] = 7×15×3 = 315OPT[4,5] = 15×3×21 = 945
步骤3:长度为3的链
计算 OPT[1,3](矩阵链 A₁A₂A₃):
- 切割点 k=1:
(A₁)(A₂A₃)OPT[1,1] + OPT[2,3] + p₀×p₁×p₃ = 0 + 1155 + 6×11×15 = 0 + 1155 + 990 = 2145
- 切割点 k=2:
(A₁A₂)(A₃)OPT[1,2] + OPT[3,3] + p₀×p₂×p₃ = 462 + 0 + 6×7×15 = 462 + 0 + 630 = 1092✓(最小)
OPT[1,3] = min{2145, 1092} = 1092,K[1,3] = 2
计算 OPT[2,4](矩阵链 A₂A₃A₄):
- 切割点 k=2:
(A₂)(A₃A₄)OPT[2,2] + OPT[3,4] + p₁×p₂×p₄ = 0 + 315 + 11×7×3 = 0 + 315 + 231 = 546✓(最小)
- 切割点 k=3:
(A₂A₃)(A₄)OPT[2,3] + OPT[4,4] + p₁×p₃×p₄ = 1155 + 0 + 11×15×3 = 1155 + 0 + 495 = 1650
OPT[2,4] = min{546, 1650} = 546,K[2,4] = 2
计算 OPT[3,5](矩阵链 A₃A₄A₅):
- 切割点 k=3:
(A₃)(A₄A₅)OPT[3,3] + OPT[4,5] + p₂×p₃×p₅ = 0 + 945 + 7×15×21 = 0 + 945 + 2205 = 3150
- 切割点 k=4:
(A₃A₄)(A₅)OPT[3,4] + OPT[5,5] + p₂×p₄×p₅ = 315 + 0 + 7×3×21 = 315 + 0 + 441 = 756✓(最小)
OPT[3,5] = min{3150, 756} = 756,K[3,5] = 4
步骤4:长度为4的链
计算 OPT[1,4](矩阵链 A₁A₂A₃A₄):
- 切割点 k=1:
(A₁)(A₂A₃A₄)OPT[1,1] + OPT[2,4] + p₀×p₁×p₄ = 0 + 546 + 6×11×3 = 0 + 546 + 198 = 744✓(最小)
- 切割点 k=2:
(A₁A₂)(A₃A₄)OPT[1,2] + OPT[3,4] + p₀×p₂×p₄ = 462 + 315 + 6×7×3 = 462 + 315 + 126 = 903
- 切割点 k=3:
(A₁A₂A₃)(A₄)OPT[1,3] + OPT[4,4] + p₀×p₃×p₄ = 1092 + 0 + 6×15×3 = 1092 + 0 + 270 = 1362
OPT[1,4] = min{744, 903, 1362} = 744,K[1,4] = 1
计算 OPT[2,5](矩阵链 A₂A₃A₄A₅):
- 切割点 k=2:
(A₂)(A₃A₄A₅)OPT[2,2] + OPT[3,5] + p₁×p₂×p₅ = 0 + 756 + 11×7×21 = 0 + 756 + 1617 = 2373
- 切割点 k=3:
(A₂A₃)(A₄A₅)OPT[2,3] + OPT[4,5] + p₁×p₃×p₅ = 1155 + 945 + 11×15×21 = 1155 + 945 + 3465 = 5565
- 切割点 k=4:
(A₂A₃A₄)(A₅)OPT[2,4] + OPT[5,5] + p₁×p₄×p₅ = 546 + 0 + 11×3×21 = 546 + 0 + 693 = 1239✓(最小)
OPT[2,5] = min{2373, 5565, 1239} = 1239,K[2,5] = 4
步骤5:长度为5的链(最终结果)
计算 OPT[1,5](矩阵链 A₁A₂A₃A₄A₅):
- 切割点 k=1:
(A₁)(A₂A₃A₄A₅)OPT[1,1] + OPT[2,5] + p₀×p₁×p₅ = 0 + 1239 + 6×11×21 = 0 + 1239 + 1386 = 2625
- 切割点 k=2:
(A₁A₂)(A₃A₄A₅)OPT[1,2] + OPT[3,5] + p₀×p₂×p₅ = 462 + 756 + 6×7×21 = 462 + 756 + 882 = 2100
- 切割点 k=3:
(A₁A₂A₃)(A₄A₅)OPT[1,3] + OPT[4,5] + p₀×p₃×p₅ = 1092 + 945 + 6×15×21 = 1092 + 945 + 1890 = 3927
- 切割点 k=4:
(A₁A₂A₃A₄)(A₅)OPT[1,4] + OPT[5,5] + p₀×p₄×p₅ = 744 + 0 + 6×3×21 = 744 + 0 + 378 = 1122✓(最小)
OPT[1,5] = min{2625, 2100, 3927, 1122} = 1122,K[1,5] = 4
4. 结果与回溯
最优连乘次数:1122
最优连乘顺序:根据K表回溯得到 ((A(B(CD)))E)
验证:按此顺序计算
- C×D:7×15×3 = 315
- B×(CD):11×7×3 = 231
- A×(B(CD)):6×11×3 = 198
- (A(B(CD)))×E:6×3×21 = 378
总计:315 + 231 + 198 + 378 = 1122 ✓
5. 复杂度分析
- 时间复杂度:O(n³)(三重循环)
- 空间复杂度:O(n²)(存储OPT表和K表)
关键洞察
- 中间结果维度很重要:尽量让中间结果维度小,后续计算更省
- 自底而上填表:确保计算
OPT(i, j)时,所有子问题都已计算完成 - 枚举所有切割点:因为不知道哪个切割点最优,必须全部枚举
- 最优子结构:全局最优解必须由局部最优解组成
三、问题二:钢条切割问题(动态规划简化版)
问题描述
给定一根长度为n英寸的钢条,将其切割成整数长度的段,每段可以单独出售。价格表如下:
| 长度(英寸) | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 价格(元) | 1 | 4 | 4 | 7 | 8 | 9 |
目标:设计动态规划算法,使总收益最大化。
问题特性
- 最优子结构:长度为n的钢条的最优切割方案,包含子段的最优切割方案
- 重叠子问题:不同长度的切割方案会共享相同的子问题
- 一维状态:状态只依赖长度这一个维度,比矩阵连乘更简单
动态规划算法设计
1. 状态定义
r[i] = 长度为 i 的钢条的最大收益
2. 状态转移方程
价格数组定义:p[j] 表示长度为 j 的钢条段的价格(直接出售,不切割)
根据价格表:
p[1] = 1(长度为1英寸的钢条价格1元)p[2] = 4(长度为2英寸的钢条价格4元)p[3] = 4(长度为3英寸的钢条价格4元)p[4] = 7(长度为4英寸的钢条价格7元)p[5] = 8(长度为5英寸的钢条价格8元)p[6] = 9(长度为6英寸的钢条价格9元)
状态转移方程:
r[i] = max{p[j] + r[i-j] | 1 ≤ j ≤ i}
逻辑解释:
- 对于长度为i的钢条,枚举第一段的长度j(1到i)
- 如果第一段长度为j,收益为:
p[j]:第一段(长度为j)直接出售的价格r[i-j]:剩余部分(长度为i-j)的最优切割收益- 总收益 =
p[j] + r[i-j]
- 选择使总收益最大的j
3. 填表过程(n=6)
初始化:r[0] = 0(长度为0的钢条收益为0)
i=1(长度为1英寸的钢条):
- j=1:第一段长度为1,
p[1] + r[0] = 1 + 0 = 1✓(唯一选择) r[1] = 1,切割方案:不切割,整根出售(1英寸)
i=2(长度为2英寸的钢条):
- j=1:第一段长度为1,
p[1] + r[1] = 1 + 1 = 2- 方案:切成1+1,收益 = 1 + 1 = 2
- j=2:第一段长度为2,
p[2] + r[0] = 4 + 0 = 4✓(最大)- 方案:不切割,整根出售(2英寸),收益 = 4
r[2] = max{2, 4} = 4,最优方案:不切割,整根出售
i=3(长度为3英寸的钢条):
- j=1:第一段长度为1,
p[1] + r[2] = 1 + 4 = 5✓(最大)- 方案:切成1+2,收益 = 1 + 4 = 5
- j=2:第一段长度为2,
p[2] + r[1] = 4 + 1 = 5✓(同样最大)- 方案:切成2+1,收益 = 4 + 1 = 5
- j=3:第一段长度为3,
p[3] + r[0] = 4 + 0 = 4- 方案:不切割,整根出售(3英寸),收益 = 4
r[3] = max{5, 5, 4} = 5,最优方案:切成1+2或2+1
i=4(长度为4英寸的钢条):
- j=1:第一段长度为1,
p[1] + r[3] = 1 + 5 = 6- 方案:切成1+3,收益 = 1 + 5 = 6
- j=2:第一段长度为2,
p[2] + r[2] = 4 + 4 = 8✓(最大)- 方案:切成2+2,收益 = 4 + 4 = 8
- j=3:第一段长度为3,
p[3] + r[1] = 4 + 1 = 5- 方案:切成3+1,收益 = 4 + 1 = 5
- j=4:第一段长度为4,
p[4] + r[0] = 7 + 0 = 7- 方案:不切割,整根出售(4英寸),收益 = 7
r[4] = max{6, 8, 5, 7} = 8,最优方案:切成2+2
i=5(长度为5英寸的钢条):
- j=1:第一段长度为1,
p[1] + r[4] = 1 + 8 = 9✓(最大)- 方案:切成1+4,收益 = 1 + 8 = 9
- j=2:第一段长度为2,
p[2] + r[3] = 4 + 5 = 9✓(同样最大)- 方案:切成2+3,收益 = 4 + 5 = 9
- j=3:第一段长度为3,
p[3] + r[2] = 4 + 4 = 8- 方案:切成3+2,收益 = 4 + 4 = 8
- j=4:第一段长度为4,
p[4] + r[1] = 7 + 1 = 8- 方案:切成4+1,收益 = 7 + 1 = 8
- j=5:第一段长度为5,
p[5] + r[0] = 8 + 0 = 8- 方案:不切割,整根出售(5英寸),收益 = 8
r[5] = max{9, 9, 8, 8, 8} = 9,最优方案:切成1+4或2+3
i=6(长度为6英寸的钢条):
- j=1:第一段长度为1,
p[1] + r[5] = 1 + 9 = 10- 方案:切成1+5,收益 = 1 + 9 = 10
- j=2:第一段长度为2,
p[2] + r[4] = 4 + 8 = 12✓(最大)- 方案:切成2+4,进一步2+4可以优化为2+2+2,收益 = 4 + 8 = 12
- j=3:第一段长度为3,
p[3] + r[3] = 4 + 5 = 9- 方案:切成3+3,收益 = 4 + 5 = 9
- j=4:第一段长度为4,
p[4] + r[2] = 7 + 4 = 11- 方案:切成4+2,收益 = 7 + 4 = 11
- j=5:第一段长度为5,
p[5] + r[1] = 8 + 1 = 9- 方案:切成5+1,收益 = 8 + 1 = 9
- j=6:第一段长度为6,
p[6] + r[0] = 9 + 0 = 9- 方案:不切割,整根出售(6英寸),收益 = 9
r[6] = max{10, 12, 9, 11, 9, 9} = 12,最优方案:切成2+2+2(三个2英寸段)
4. 结果
最大收益:r[6] = 12元
最优切割方案:切成3段,每段2英寸(2 + 2 + 2 = 6)
5. 复杂度分析
- 时间复杂度:O(n²)
- 空间复杂度:O(n)
与矩阵连乘的对比
| 特性 | 矩阵连乘 | 钢条切割 |
|---|---|---|
| 状态维度 | 二维 OPT(i,j) | 一维 r[i] |
| 状态转移 | 枚举切割点k | 枚举第一段长度j |
| 复杂度 | O(n³) | O(n²) |
| 难度 | 较复杂 | 较简单 |
四、问题三:苹果买卖问题(贪心算法案例)
问题描述
一辆卡车从城市A到城市B,途经n个苹果市场。在每个市场i,可以以价格B[i]买入苹果,或以价格S[i]卖出苹果。
目标:在某个市场i买入,在后续市场j(j ≥ i)卖出,使利润 M = S[j] - B[i] 最大化。
约束:
- 卡车只能前进,不能后退
- 只能进行一次买入和一次卖出
- 可以在同一市场买入和卖出
为什么用贪心算法?
问题特性分析:
- 最优子结构:如果(i, j)是最优买卖对,那么i一定是1到j之间价格最低的市场
- 贪心选择特性:对于每个卖出位置j,最优买入位置是j之前(包括j)价格最低的市场
- 局部最优保证全局最优:维护最低买入价格,计算每个位置的最大利润
与动态规划的区别:
- 不需要枚举所有可能的买卖对
- 只需要一次遍历,维护最低买入价格
- 复杂度从O(n²)降为O(n)
贪心算法设计
1. 核心思路
对于每个卖出位置j,最优买入位置是j之前(包括j)价格最低的市场。
2. 算法伪代码
输入:
B[1..n]:每个市场的买入价格数组S[1..n]:每个市场的卖出价格数组n:市场数量
输出:
optimalBuyMarket:最优买入市场编号sellMarket:最优卖出市场编号maxProfit:最大利润
变量说明:
minBuy:到当前位置为止的最低买入价格buyMarket:当前最低买入价格对应的市场编号maxProfit:到目前为止的最大利润sellMarket:当前最大利润对应的卖出市场编号optimalBuyMarket:当前最大利润对应的买入市场编号
APPLE-TRADING(B, S, n)
1. minBuy ← B[1] // 初始化:第一个市场的最低买入价格就是B[1]2. buyMarket ← 1 // 初始化:第一个市场作为当前最低买入价格的市场3. maxProfit ← S[1] - B[1] // 初始化:第一个市场的利润(可能为负,表示亏损)4. sellMarket ← 1 // 初始化:第一个市场作为当前最优卖出市场5. // 从第二个市场开始遍历6. for j ← 2 to n// 遍历每个市场j,作为潜在的卖出市场7. do if B[j] < minBuy// 如果市场j的买入价格低于当前最低买入价格8. then minBuy ← B[j]// 更新最低买入价格为B[j]9. buyMarket ← j// 更新最低买入价格对应的市场为j// 注意:这里更新buyMarket,但optimalBuyMarket可能不变// 因为虽然找到了更低的买入价,但可能利润不是最大的10. // 计算在市场j卖出的利润// 使用当前已知的最低买入价格(可能在市场j之前)11. profit ← S[j] - minBuy// 利润 = 市场j的卖出价格 - 到j为止的最低买入价格// 这保证了买入市场 ≤ 卖出市场j(满足约束条件)12. if profit > maxProfit// 如果当前利润大于已知的最大利润13. then maxProfit ← profit// 更新最大利润14. sellMarket ← j// 更新最优卖出市场为j15. optimalBuyMarket ← buyMarket// 更新最优买入市场为当前buyMarket// 注意:这里buyMarket是到j为止的最低买入价格对应的市场// 这保证了(optimalBuyMarket, sellMarket)是最优买卖对16.
17. return (optimalBuyMarket, sellMarket, maxProfit)// 返回最优买入市场、最优卖出市场和最大利润
算法核心逻辑:
-
维护最低买入价格(第7-9行):在遍历过程中,始终记录到当前位置为止的最低买入价格及其对应的市场。这是贪心选择的关键——对于每个卖出位置j,最优买入位置一定是j之前(包括j)价格最低的市场。
-
计算当前利润(第11行):对于每个市场j,计算"在最低买入价格的市场买入,在市场j卖出"的利润。这利用了贪心选择特性,不需要枚举所有可能的买入位置。
-
更新最优解(第12-15行):如果当前利润大于已知的最大利润,更新最大利润和对应的买卖市场对。
为什么这样设计是正确的?
- 对于任意卖出位置j,如果我们已经知道j之前(包括j)的最低买入价格是minBuy,对应的市场是buyMarket,那么(buyMarket, j)就是在j卖出的最优买卖对。
- 因为如果存在更优的买入位置k(k < j且B[k] < minBuy),那么算法应该已经在遍历到k时更新了minBuy和buyMarket。
- 因此,我们只需要遍历一次,对每个卖出位置j,用当前已知的最低买入价格计算利润即可。
3. 算法执行示例
假设输入:
- B = [5, 3, 2, 4, 1, 6] // 买入价格
- S = [4, 6, 5, 7, 3, 8] // 卖出价格
执行过程详解:
列说明:
j:当前遍历到的市场编号(作为潜在的卖出市场)B[j]:市场j的买入价格S[j]:市场j的卖出价格minBuy:到市场j为止的最低买入价格buyMarket:当前最低买入价格对应的市场编号profit:在市场j卖出的利润(= S[j] - minBuy)maxProfit:到目前为止的最大利润sellMarket:当前最大利润对应的卖出市场optimalBuyMarket:当前最大利润对应的买入市场
逐步执行过程:
j=1(初始化,处理第一个市场):
B[1] = 5,S[1] = 4minBuy = 5(第一个市场,最低买入价格就是B[1])buyMarket = 1(最低买入价格对应的市场)profit = S[1] - minBuy = 4 - 5 = -1(亏损1元)maxProfit = -1(当前最大利润,虽然是亏损)sellMarket = 1,optimalBuyMarket = 1- 含义:如果只在市场1买卖,会亏损1元
j=2(处理第二个市场):
B[2] = 3,S[2] = 6- 更新最低买入价格:因为
B[2] = 3 < minBuy = 5,所以:minBuy = 3(更新为更低的价格)buyMarket = 2(更新为市场2)
profit = S[2] - minBuy = 6 - 3 = 3(利润3元)- 更新最大利润:因为
profit = 3 > maxProfit = -1,所以:maxProfit = 3sellMarket = 2(最优卖出市场更新为市场2)optimalBuyMarket = 2(最优买入市场更新为市场2)
- 含义:在市场2买入(价格3),在市场2卖出(价格6),利润3元。这是目前最优方案。
j=3(处理第三个市场):
B[3] = 2,S[3] = 5- 更新最低买入价格:因为
B[3] = 2 < minBuy = 3,所以:minBuy = 2(更新为更低的价格)buyMarket = 3(更新为市场3)
profit = S[3] - minBuy = 5 - 2 = 3(利润3元)- 不更新最大利润:因为
profit = 3 = maxProfit = 3,没有更优 sellMarket = 2,optimalBuyMarket = 2(保持不变)- 含义:虽然找到了更低的买入价格(市场3,价格2),但在市场3卖出的利润(3元)没有超过当前最优利润(3元),所以最优方案仍然是(市场2,市场2)。
j=4(处理第四个市场):
B[4] = 4,S[4] = 7- 不更新最低买入价格:因为
B[4] = 4 > minBuy = 2,所以minBuy和buyMarket保持不变 profit = S[4] - minBuy = 7 - 2 = 5(利润5元)- 注意:这里使用的是市场3的买入价格(minBuy=2),而不是市场4的买入价格
- 更新最大利润:因为
profit = 5 > maxProfit = 3,所以:maxProfit = 5sellMarket = 4(最优卖出市场更新为市场4)optimalBuyMarket = 3(最优买入市场更新为市场3,即当前的buyMarket)
- 含义:在市场3买入(价格2),在市场4卖出(价格7),利润5元。这是新的最优方案。
j=5(处理第五个市场):
B[5] = 1,S[5] = 3- 更新最低买入价格:因为
B[5] = 1 < minBuy = 2,所以:minBuy = 1(更新为更低的价格)buyMarket = 5(更新为市场5)
profit = S[5] - minBuy = 3 - 1 = 2(利润2元)- 不更新最大利润:因为
profit = 2 < maxProfit = 5,所以最优方案保持不变 sellMarket = 4,optimalBuyMarket = 3(保持不变)- 含义:虽然找到了更低的买入价格(市场5,价格1),但在市场5卖出的利润(2元)没有超过当前最优利润(5元),所以最优方案仍然是(市场3,市场4)。
j=6(处理第六个市场,最终结果):
B[6] = 6,S[6] = 8- 不更新最低买入价格:因为
B[6] = 6 > minBuy = 1,所以minBuy和buyMarket保持不变 profit = S[6] - minBuy = 8 - 1 = 7(利润7元)- 注意:这里使用的是市场5的买入价格(minBuy=1),而不是市场6的买入价格
- 更新最大利润:因为
profit = 7 > maxProfit = 5,所以:maxProfit = 7sellMarket = 6(最优卖出市场更新为市场6)optimalBuyMarket = 5(最优买入市场更新为市场5,即当前的buyMarket)
- 含义:在市场5买入(价格1),在市场6卖出(价格8),利润7元。这是最终的最优方案。
最终结果:
- 最优买入市场:5(价格1元/斤)
- 最优卖出市场:6(价格8元/斤)
- 最大利润:7元/斤
关键洞察:
- minBuy和buyMarket的更新:每当遇到更低的买入价格时更新,这保证了对于每个卖出位置j,我们总是使用j之前(包括j)的最低买入价格。
- profit的计算:总是使用当前的minBuy计算,而不是B[j],这体现了贪心选择特性。
- 最优解的更新:只有当profit大于maxProfit时才更新,这保证了我们找到的是全局最优解。
4. 正确性证明
贪心选择特性:对于任意卖出位置j,最优买入位置一定是j之前(包括j)价格最低的市场。
证明(反证法):
- 假设存在更优的买入位置k(k < i且B[k] < B[i]),其中i是j之前价格最低的市场
- 那么(k, j)的利润 = S[j] - B[k] > S[j] - B[i] = (i, j)的利润
- 但这与"i是j之前价格最低的市场"矛盾,因为如果B[k] < B[i],算法应该已经将minBuy更新为B[k]
5. 复杂度分析
- 时间复杂度:O(n)(单次遍历)
- 空间复杂度:O(1)(只使用常数个变量)
与动态规划的对比
如果用动态规划:
- 状态定义:
F[i][j]= 在市场i买入、市场j卖出的利润 - 需要计算所有O(n²)个状态
- 复杂度:O(n²)
贪心算法的优势:
- 只需要O(n)时间
- 代码更简单
- 空间复杂度O(1)
五、三种问题的算法选择决策树
问题是否满足最优子结构?
├─ 否 → 不适合用DP或贪心(考虑其他算法)
└─ 是 → 是否满足贪心选择特性?├─ 是 → 优先用贪心算法│ ├─ 苹果买卖:O(n)时间,O(1)空间│ └─ 区间调度(无权重):O(n log n)时间└─ 否 → 用动态规划├─ 矩阵连乘:O(n³)时间,O(n²)空间└─ 钢条切割:O(n²)时间,O(n)空间
问题特征对比表
| 问题 | 算法类型 | 状态维度 | 时间复杂度 | 空间复杂度 | 关键特性 |
|---|---|---|---|---|---|
| 矩阵连乘 | 动态规划 | 二维 | O(n³) | O(n²) | 重叠子问题、最优子结构 |
| 钢条切割 | 动态规划 | 一维 | O(n²) | O(n) | 重叠子问题、最优子结构 |
| 苹果买卖 | 贪心算法 | 无状态表 | O(n) | O(1) | 贪心选择特性、最优子结构 |
六、总结:算法选择的决策框架
核心原则
- 先判断最优子结构:如果不满足,不适合用DP或贪心
- 再判断贪心选择特性:如果满足,优先用贪心(通常更简单高效)
- 最后选择动态规划:如果只满足最优子结构,用DP
性能对比
| 算法类型 | 时间复杂度 | 空间复杂度 | 实现难度 | 适用场景 |
|---|---|---|---|---|
| 贪心算法 | O(n) 或 O(n log n) | O(1) 或 O(n) | 简单 | 满足贪心选择特性 |
| 动态规划 | O(n²) 或 O(n³) | O(n) 或 O(n²) | 中等 | 满足最优子结构 |
