相关笔记

概览

钢条切割问题是动态规划的经典入门案例。给定一根长度为 的钢条和一个价格表 ,目标是找到一种切割方案,使得销售收益最大化。本节通过自顶向下(备忘录法)自底向上两种方法,展示动态规划的最优子结构重叠子问题两大核心性质,并给出 的时间复杂度分析。

核心要点:

  • 最优子结构:最优解包含子问题的最优解
  • 重叠子问题:递归过程中反复求解相同子问题
  • 备忘录法:自顶向下 + 缓存已计算结果
  • 自底向上法:按规模从小到大依次求解
  • 时间复杂度:

知识结构总览

flowchart TD
    A["钢条切割问题"] --> B["问题定义"]
    A --> C["朴素递归解法"]
    A --> D["动态规划解法"]
    A --> E["重构最优解"]

    B --> B1["长度 n 的钢条"]
    B --> B2["价格表 p[i], i=1..n"]
    B --> B3["目标:最大收益 r_n"]

    C --> C1["递归公式"]
    C --> C2["指数级时间复杂度"]
    C --> C3["重叠子问题导致低效"]

    D --> D1["自顶向下(备忘录法)"]
    D --> D2["自底向上法"]

    D1 --> D1a["MEMOIZED-CUT-ROD"]
    D1 --> D1b["只计算需要的子问题"]
    D1 --> D1c["Θ(n²)"]

    D2 --> D2a["BOTTOM-UP-CUT-ROD"]
    D2 --> D2b["按规模递增求解"]
    D2 --> D2c["Θ(n²)"]

    E --> E1["记录切割点 s[i]"]
    E --> E2["EXTENDED-BOTTOM-UP-CUT-ROD"]
    E --> E3["PRINT-CUT-ROD-SOLUTION"]

核心思想

核心思路

钢条切割问题的核心在于识别最优子结构性质:长度为 的钢条的最优切割方案,必然包含长度为 的子钢条的最优切割方案。这意味着我们可以将大问题分解为小问题,而小问题的最优解组合起来就是大问题的最优解。同时,由于不同切割方案会反复需要相同长度的子问题解,这就产生了重叠子问题。动态规划通过缓存子问题的解(备忘录法)或按规模从小到大依次求解(自底向上法),将指数级的递归搜索压缩到多项式时间。关键公式为 ,其中 作为递归基。这一公式告诉我们:对于长度为 的钢条,要么不切割直接卖 ,要么在位置 处切一刀,左段卖 ,右段求最优收益 ,遍历所有可能的切割位置取最大值。

自顶向下(备忘录法)—— 伪代码

算法执行流程

  1. 初始化备忘录数组 r[0..n] 全部为负无穷
  2. 对每个长度 i 从 1 到 n:调用辅助函数 MEMOIZED-CUT-ROD-AUX
  3. 辅助函数:若 r[n] 已计算(>= 0),直接返回
  4. 基准情形:n == 0 时返回 0
  5. 递归情形:遍历每个切割位置 i 从 1 到 n,计算 max(p[i] + 辅助函数(n-i))
  6. 将最优收益存入 r[n],记录最优切割位置 s[n]
flowchart TD
    A["初始化 r[0..n] = -inf"] --> B["调用 AUX p, n, r, s"]
    B --> C{"r[n] >= 0?"}
    C -- "是" --> D["返回 r[n]"]
    C -- "否" --> E{"n == 0?"}
    E -- "是" --> F["q = 0"]
    E -- "否" --> G["q = -inf, i = 1"]
    G --> H{"i <= n?"}
    H -- "是" --> I["q = max q, p[i]+AUX n-i"]
    I --> J["i = i + 1"]
    J --> H
    H -- "否" --> K["r[n] = q<br/>s[n] = 最优切割位置"]
    F --> K
    K --> D
MEMOIZED-CUT-ROD(p, n)
1  let r[0..n] and s[0..n] be new arrays
2  for i = 0 to n
3     r[i] = -∞
4  return MEMOIZED-CUT-ROD-AUX(p, n, r, s)

MEMOIZED-CUT-ROD-AUX(p, n, r, s)
1  if r[n] ≥ 0
2     return r[n]
3  if n == 0
4     q = 0
5  else q = -∞
6     for i = 1 to n
7        q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n-i, r, s))
8        s[n] = i  // 记录最优切割长度
9  r[n] = q
10 return q

自底向上法 —— 伪代码

算法执行流程

  1. 初始化 r[0] = 0
  2. 外层循环:对每个长度 j 从 1 到 n
  3. 初始化 q = 负无穷
  4. 内层循环:对每个切割位置 i 从 1 到 j
  5. 计算 q = max(q, p[i] + r[j-i])(左段卖 p[i],右段最优收益 r[j-i])
  6. 将最优收益存入 r[j]
  7. 返回 r[n]
flowchart TD
    A["r[0] = 0, j = 1"] --> B{"j <= n?"}
    B -- "是" --> C["q = -inf, i = 1"]
    C --> D{"i <= j?"}
    D -- "是" --> E["q = max q, p[i]+r[j-i]"]
    E --> F["i = i + 1"]
    F --> D
    D -- "否" --> G["r[j] = q"]
    G --> H["j = j + 1"]
    H --> B
    B -- "否" --> I["返回 r[n]"]
BOTTOM-UP-CUT-ROD(p, n)
1  let r[0..n] be new array
2  r[0] = 0
3  for j = 1 to n
4     q = -∞
5     for i = 1 to j
6        q = max(q, p[i] + r[j-i])
7     r[j] = q
8  return r[j]

扩展版自底向上(含切割方案重构)—— 伪代码

算法执行流程

  1. 初始化 r[0] = 0
  2. 外层循环:对每个长度 j 从 1 到 n
  3. 内层循环:对每个切割位置 i 从 1 到 j
  4. 若 p[i] + r[j-i] > q,更新 q 和 记录最优切割位置 s[j] = i
  5. 将最优收益存入 r[j]
  6. 返回 r 和 s(s 用于后续重构切割方案)
flowchart TD
    A["r[0] = 0, j = 1"] --> B{"j <= n?"}
    B -- "是" --> C["q = -inf, i = 1"]
    C --> D{"i <= j?"}
    D -- "是" --> E{"p[i]+r[j-i] > q?"}
    E -- "是" --> F["q = p[i]+r[j-i]<br/>s[j] = i"]
    E -- "否" --> G["i = i + 1"]
    F --> G
    G --> D
    D -- "否" --> H["r[j] = q"]
    H --> I["j = j + 1"]
    I --> B
    B -- "否" --> J["返回 r 和 s"]
EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
1  let r[0..n] and s[0..n] be new arrays
2  r[0] = 0
3  for j = 1 to n
4     q = -∞
5     for i = 1 to j
6        if q < p[i] + r[j-i]
7           q = p[i] + r[j-i]
8           s[j] = i
9     r[j] = q
10 return r and s

PRINT-CUT-ROD-SOLUTION(p, n)
1  (r, s) = EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
2  while n > 0
3     print s[n]
4     n = n - s[n]

最优子结构定义

一个问题具有最优子结构性质,如果该问题的最优解可以由其子问题的最优解高效地构造出来。对于钢条切割问题,设 为长度 的钢条的最大收益,则:

其中 。该公式表明,长度为 的最优方案要么不切割(收益为 ),要么在某个位置 切割后,左段直接出售获得 ,右段以最优方式切割获得

重叠子问题定义

当一个递归算法不断调用相同的子问题时,我们称该问题具有重叠子问题性质。在钢条切割的朴素递归中,计算 需要计算 ,而计算 又需要计算 ,子问题被反复求解,导致时间复杂度呈指数增长。动态规划通过存储已求解的子问题结果来消除这种冗余。

循环不变式与正确性证明

循环不变式

自底向上法 BOTTOM-UP-CUT-ROD 的外层循环不变式:

在第 次外层循环迭代开始时, 存储了长度为 )的钢条的最大收益。

【初始化(j=1 时 r[0]=0 已正确设置)】 时, 已被正确设置。对于 ,即 是长度为 0 的钢条的最大收益(不切割、不售卖),不变式成立。

【维护(j-i < j 保证 r[j-i] 已正确计算)】 假设在第 次迭代开始时不变式成立,即 都已存储了正确的最大收益。内层循环遍历 ,对每个 计算 。由于 已经是正确的最优收益(由不变式保证)。因此 就是 的正确值,赋值后不变式对 成立。

【终止(j=n+1 时 r[0..n] 全部正确)】 时循环结束。此时 对所有 都存储了正确的最大收益,特别地 就是所求答案。

时间复杂度分析

时间复杂度

自底向上法 BOTTOM-UP-CUT-ROD 外层循环执行 次(),内层循环在第 次外层迭代中执行 次()。总操作次数为

自顶向下备忘录法 MEMOIZED-CUT-ROD 每个子问题 )最多被求解一次。求解 需要遍历 个切割位置,因此求解 的时间为 。总时间为

朴素递归(无备忘录): 递归调用形成一棵二叉树,最坏情况下调用次数为 级别,时间复杂度为


补充理解与拓展

动态规划的发明历史

**“动态规划”**这一名称由美国数学家 Richard Bellman 于 1950 年代提出。Bellman 在其自传 Eye of the Hurricane 中回忆了命名的过程:当时他在兰德公司(RAND Corporation)工作,需要为他的多阶段决策过程研究申请经费。当时的美国国防部长 Charles Erwin Wilson 非常厌恶”研究(research)“一词,认为它听起来像是在浪费纳税人的钱。Bellman 需要一个不会引起反感的名称。他选择了”dynamic”(动态的),因为”没有人能反对这个词”,而”programming”在当时更多指”规划/调度”而非计算机编程1

Bellman 的核心贡献在于提出了最优性原理(Principle of Optimality):一个最优策略的任何子策略,对于其子问题的初始状态和终止状态而言,也必须是最优的。这正是钢条切割问题中”最优子结构”的理论基础。

备忘录法 vs 自底向上法的工程选择

两种方法在渐近时间复杂度上相同(均为 ),但在实际工程中有不同的权衡:

备忘录法的优势: 只计算实际需要的子问题。如果某些子问题不会被用到,备忘录法可以跳过它们。代码结构更接近问题的自然递归定义,可读性更好。

自底向上法的优势: 没有递归调用的栈开销,在实际运行中常数因子更小。可以更方便地利用数组的局部性(cache友好),因为访问模式是顺序的。不需要处理递归深度限制的问题。

在实际工程中,自底向上法通常是首选,除非问题规模非常大且只有少量子问题会被实际访问。例如,在竞争编程中,自底向上法几乎总是更快;而在某些交互式场景或子问题空间稀疏的情况下,备忘录法更为合适。


易混淆点与辨析

"动态规划"中的"programming"不是计算机编程

❌ 错误理解:动态规划是一种编程技术,“programming”指的是写代码。 ✅ 正确理解:在 Bellman 时代,“programming”指的是”规划”或”调度”(源自 mathematical programming,即数学规划/最优化)。动态规划是一种最优化方法,其名称与计算机编程无关。

贪心法 vs 动态规划

❌ 错误理解:钢条切割问题可以用贪心法解决——每次选择单位长度价格最高的切割方案即可。 ✅ 正确理解:贪心法不能保证钢条切割问题的最优解。考虑价格表 (对应长度 1-10),长度为 4 时,贪心法会选择切割为 4 段长度 1(收益 4)或 2 段长度 2(收益 10),但最优解是不切割直接卖长度 4(收益 9)或切割为 2+2(收益 10)。对于长度 10,贪心法可能无法找到最优的切割方案(如 10=2+2+6,收益 22)。贪心法要求贪心选择性质,而钢条切割问题不满足这一性质。

备忘录法不是分治法

❌ 错误理解:备忘录法只是分治法的一种优化,本质上还是分治。 ✅ 正确理解:分治法要求子问题相互独立(不重叠),而动态规划专门处理子问题重叠的情况。备忘录法通过缓存消除重叠子问题的冗余计算,这使得它在本质上属于动态规划方法,而非分治法。两者的关键区别在于子问题是否重叠。


习题精选

题号题目描述难度
14.1-1对价格表 ,求长度 的最优收益和切割方案
14.1-2修改 BOTTOM-UP-CUT-ROD,使其不仅返回最大收益,还返回切割方案⭐⭐
14.1-3证明:对任意价格表,存在一个最优切割方案,不切割长度为 的钢条,而是将长度 的钢条分成两部分⭐⭐
14.1-4给定一个长度为 的钢条,允许对每段切割收取固定费用 ,修改算法以考虑此费用⭐⭐
14.1-5给定钢条长度 和价格表 ,以及每段钢条的成本 (每段成本相同),求最大利润⭐⭐
14.1-6给定钢条长度 和价格表 ,限制最多切割 次,求最大收益⭐⭐⭐
14.1-7给定钢条长度 和价格表 ,要求每段长度必须是给定集合 中的元素,求最大收益⭐⭐⭐

视频学习指南

资源名称讲者/来源时长链接特点
MIT 6.006 Lecture 11: Dynamic ProgrammingErik Demaine~75minYouTubeMIT经典课程,从钢条切割引入DP
CLRS 动态规划系列 - 钢条切割abdul bari~18minYouTube直观的表格填充过程演示
算法导论精讲 - 动态规划王晓东~40minB站中文讲解,配合教材推导
Dynamic Programming ICS Dojo~25minYouTube从零开始讲解DP思想
国防科大算法课 - 动态规划国防科技大学~50minB站中文,包含大量例题

教材原文

CLRS 第4版 14.1节原文

钢条切割问题。Serling公司购买长钢条,将其切割为短钢条出售。切割工序本身没有成本。公司管理层希望知道最佳的切割方案。假定我们知道对于 的一段长度为 英寸的钢条的价格为 。长度为 英寸的钢条的价格 如表14-1所示。

考虑长度为 英寸的钢条,我们可以使用一种组合优化方法来找到最佳切割方案。对于 ,设 为长度为 的钢条的最大收益。对于最优切割方案,我们可以将其分解为:要么不切割直接出售(收益为 ),要么在某处切割后分别对两段求最优收益。因此,最优收益满足递归关系:

其中 。这种递归关系展示了该问题的最优子结构性质。

如果直接使用递归求解,由于子问题重叠,算法效率极低。动态规划通过两种方法来高效求解:自顶向下带备忘录的方法和自底向上的方法。两种方法的时间复杂度均为


参见Wiki

第14章-动态规划 钢条切割

Footnotes

  1. Bellman, R. E. (1984). Eye of the Hurricane: An Autobiography. World Scientific, pp. 159-164.