Skip to content

第六章 动态规划——如何管理子问题依赖

开篇:三个"看似简单"的问题

问题 1:Fibonacci 数列

看起来简单:"不就是递归吗?"

python
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

小明在面试时自信地写下这个代码。面试官说:"算 fib(50) 看看。"

程序运行了 10 分钟还没出结果。

困惑:同样的逻辑,为什么 fib(20) 一秒完成,fib(50) 却要几分钟?


问题 2:钢条切割

某钢材公司需要切割不同长度的钢条出售。给定价格表:

长度(英寸)12345678910
价格(美元)1589101717202430

一根 4 英寸钢条的最优切割方式是什么?

小明用递归尝试所有切割方案:

python
def cut_rod(price, n):
    if n == 0:
        return 0
    max_val = -infinity
    for i in range(1, n+1):
        max_val = max(max_val, price[i] + cut_rod(price, n-i))
    return max_val

问题:一根 10 英寸的钢条,程序跑了 5 秒。一根 30 英寸的,程序跑了 10 分钟。

困惑:为什么长度增加,时间爆炸式增长?


问题 3:最长公共子序列

给定两个字符串 "ABCDGH" 和 "AEDFHR",找出最长公共子序列。

看起来简单:"逐个字符比对呗。"

小明写了个递归:

python
def lcs(X, Y, m, n):
    if m == 0 or n == 0:
        return 0
    if X[m-1] == Y[n-1]:
        return 1 + lcs(X, Y, m-1, n-1)
    else:
        return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n))

问题:长度 10 的字符串,秒算。长度 30 的字符串,几分钟。

困惑:为什么同样的问题,规模稍微增大就不可接受?


War Story:2024 年算法竞赛的"指数陷阱"

真实案例:2024 年某算法竞赛,选手 A 用递归解 Fibonacci 第 40 项。程序运行了 3 小时还没出结果。评委说:"你的分治是对的,但你的子问题画出来有指数级重复..."

选手 A 的递归树(fib(5)):

           fib(5)
          /      \
      fib(4)     fib(3)
      /   \      /    \
   fib(3) fib(2) fib(2) fib(1)
   /  \
fib(2) fib(1)

关键观察:fib(3) 计算了 2 次,fib(2) 计算了 3 次

选手 B 的记忆化版本:

python
memo = {}
def fib_memo(n):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fib_memo(n-1) + fib_memo(n-2)
    return memo[n]

同样的 fib(40),1 毫秒 完成。

效率差距:10^8 倍。


学习目标

完成本章学习后,你将能够:

  1. 识别子问题重叠:判断一个递归问题是否适合用 DP 优化
  2. 验证最优子结构:用"剪贴证明"判断 DP 是否能保证最优解
  3. 设计状态空间:从一维到多维,平衡维度与效率
  4. 实现记忆化与迭代:两种实现方式,理解各自的优劣
  5. 连接 LLM 推理:理解 KV-Cache 与 DP 记忆化的同构性

关键洞察:动态规划的核心不是"递推公式",而是发现问题结构——子问题重叠和最优子结构需要人的判断,机器做不了。


核心叙事:DP 记忆化 ↔ KV-Cache 同构

一个令人惊讶的发现

2026 年,研究者发现:大语言模型(LLM)推理时的 KV-Cache,与动态规划的记忆化本质相同

DP 概念LLM 对应共同条件
记忆化KV-Cache子问题重叠 → prefix reuse
子问题图Token DAG依赖关系 DAG
最优子结构Markov 假设状态最优不依赖路径历史
状态设计Prompt 设计影响推理效率

这不是类比,而是同构——同一计算思想的不同实例化。

为什么这个对应关系重要?

  1. 理解 DP → 理解 KV-Cache 设计决策:为什么某些 prompt 更适合缓存?
  2. 诊断 LLM 效率问题:推理慢 → 检查 prefix reuse 率
  3. 迁移优化技术:DP 的状态压缩 → KV-Cache 的 quantization

章节结构

本章遵循认知规律:

问题情境 → 直观思路 → 规范定义 → 算法实现 → 正确性分析 → 复杂度分析 → 变式练习

各节要点

6.1 子问题与重叠(~500 行)

  • 问题情境:Fibonacci 的指数陷阱——同一子问题被计算多次
  • 直观思路:画递归树,发现重复节点
  • 核心内容:子问题图、记忆化、自底向上
  • 关键代码:三种 Fibonacci 实现
  • 思考题:为什么 quicksort 不能用 DP 优化?

6.2 最优子结构(~450 行)

  • 问题情境:最长路径的失败——剪贴证明揭示答案
  • 直观思路:为什么最短路可以用 DP,最长路径不行?
  • 核心内容:剪贴证明、反例构造
  • 关键代码:最短路径的 DP 递推
  • 思考题:0-1 背包 vs 分数背包,哪个有最优子结构?

6.3 状态设计(~500 行)

  • 问题情境:状态爆炸——5 维数组导致内存不足
  • 直观思路:状态 = 参数的有效组合
  • 核心内容:一维到多维、空间优化
  • 关键代码:LCS、编辑距离、矩阵链乘
  • 思考题:如何压缩状态空间?

6.4 经典模型(~600 行)

  • 问题情境:面试现场的编辑距离优化
  • 直观思路:一个框架解决一族问题
  • 核心内容:钢条切割、矩阵链乘、编辑距离、最优 BST
  • 关键代码:Skiena 的可插拔框架
  • 思考题:Knuth 优化的条件是什么?

6.5 LLM 与 DP 同构性(~400 行)

  • 问题情境:KV-Cache 的记忆化重生
  • 直观思路:DP 记忆化 → KV-Cache 设计
  • 核心内容:四对映射、推理效率优化
  • 关键代码:推理缓存策略
  • 思考题:如何识别 prefix reuse?

综合练习(~350 行)

  • 问题情境与直觉建立(5 题)
  • 方法应用与实现(5 题)
  • 错误诊断与反例(5 题)
  • 方案比较与权衡(5 题)
  • 变式与边界条件(5 题)
  • 开放设计(5 题)
  • 综合应用(5 题)
  • 思考题(5 题)
  • LLM 协同实验(5 题)

阅读路径建议

路径一:循序渐进(推荐)

6.1 → 6.2 → 6.3 → 6.4 → 6.5 → 练习

适合:希望系统掌握动态规划的读者

路径二:LLM 导向

6.5 → 遇到概念回查前置章节 → 练习

适合:主要关注 LLM 推理优化的读者

路径三:快速入门

6.1(子问题重叠) → 6.2(最优子结构) → 练习

适合:希望快速建立核心概念的读者


关键概念预告

三个"为什么"

  1. 为什么 Fibonacci 递归这么慢?

    • 不是因为递归本身,而是因为子问题重叠——同一子问题被计算多次
    • 记忆化后:O(φ^n) → O(n)
  2. 为什么最长路径不能用 DP?

    • 因为没有最优子结构——两条最长子路径拼接后可能不合法
    • 最短路径有最优子结构:子路径最优 + 子路径最优 = 路径最优
  3. 为什么 KV-Cache 和 DP 记忆化同构?

    • 条件相同:prefix reuse = 子问题重叠
    • 有效模式识别需要人的判断

三个"如何"

  1. 如何验证最优子结构?

    • 剪贴证明:假设子解非最优 → 替换 → 矛盾
    • 构造反例:找两个最优子解拼接后不合法
  2. 如何设计状态?

    • 子问题图视角:状态 = 子问题,边 = 依赖
    • 反拓扑序求值 → 自底向上实现
  3. 如何迁移 DP 思维到 LLM?

    • 状态空间设计 → KV-Cache 管理
    • 子问题重叠识别 → prefix reuse 检测
    • 最优子结构验证 → LLM 输出评判

写作风格说明

本章遵循教材设计哲学的七大特征:

  1. 问题驱动:先让学生感受到"为什么需要",再给出定义
  2. 展示推理过程:从朴素方案到优化方案的演化,包括错误尝试
  3. 直觉先于形式:先描述直觉图景,再形式化
  4. 解释难点:回答"为什么这样定义""这个方法是怎么想到的"
  5. 例子多样化:入门例子、反例、变式、综合应用
  6. 练习多样化:不只是三层,包括错误诊断、方案比较、开放设计
  7. 终极目标:让读者面对新题时知道从哪里开始想

写在前面:动态规划最难的不是"记住递推公式",而是识别问题结构——哪些问题适合用 DP?如何验证最优子结构?本章每一节都以"问题情境"开场——先感受到困难,再理解解决思路。


参考文献

  • Cormen T H, et al. Introduction to Algorithms[M]. 4th ed. MIT Press, 2022. Chapter 15.
  • Skiena S S. The Algorithm Design Manual[M]. 3rd ed. Springer, 2020. Chapter 10.
  • Kleinberg J, Tardos E. Algorithm Design[M]. Pearson, 2005. Chapter 6.
  • Yao S, Yu D, Zhao J, et al. Tree of Thoughts[C]//NeurIPS. 2023.

新时代的算法课程