动态规划(Dynamic Programming)- 第4-5讲

北京大学软件研究所 屈婉玲 《算法设计与分析》课程 文字版PDF / PPT导出,69页 相关课程:算法分析与设计-第1讲-算法基础、算法分析与设计-第2讲-数学基础、算法分析与设计-第3讲-分治策略


一、动态规划基本思想

1.1 核心思想

动态规划的本质是多步判断求解最优化问题:

  • 将问题分解为一系列子问题,每一步做出一个决策
  • 每步决策对应一个子问题,子问题类型与原问题相同但规模更小
  • 整个决策序列对应问题的最优解

1.2 优化原则(最优子结构性质)

一个最优决策序列的任何子序列本身,一定是相对于子序列初始和结束状态的最优决策序列。

满足优化原则 → 可以用动态规划 不满足优化原则 → 不能用动态规划

反例:总长模10的最小路径问题

  • 目标:路径总长 mod 10 最小
  • 全局最优解:下、下、下、下(模10结果)
  • 动态规划给出的解:下、上、上、上
  • 原因:子路径的最优并不构成全局最优,因为模运算破坏了最优子结构

1.3 动态规划 vs 分治

特性分治动态规划
子问题关系独立不交叠重叠(大量重复子问题)
求解方式递归分解,自顶向下迭代填表,自底向上
子问题结果合并即可存储复用,避免重复计算
适用问题子问题独立即可需满足优化原则 + 子问题重叠

二、动态规划算法设计步骤

  1. 将问题表示成多步判断——分解为一系列子决策
  2. 确定优化函数和约束条件——以函数的极大/极小作为判断依据
  3. 列出递推方程和边界条件——核心数学模型
  4. 自底向上计算——从最小子问题开始填表
  5. 设立标记函数——记录最优决策路径,用于回溯构造解
  6. 时间/空间复杂度分析

三、经典应用实例

3.1 矩阵链乘法(Matrix Chain Multiplication)

问题:n个矩阵相乘 A₁A₂…Aₙ,Ai 为 Pᵢ₋₁×Pᵢ 阶矩阵,确定乘法顺序使元素相乘总次数最少。

输入:向量 P = <P₀, P₁, …, Pₙ>

实例:P = <10, 100, 5, 50>

  • (A₁A₂)A₃:10×100×5 + 10×5×50 = 7,500 次
  • A₁(A₂A₃):10×100×50 + 100×5×50 = 75,000 次
  • 相差10倍!

递推方程:

  • m[i,j] = 计算 Aᵢ…Aⱼ 的最少相乘次数
  • m[i,j] = min { m[i,k] + m[k+1,j] + Pᵢ₋₁ × Pₖ × Pⱼ } (i ≤ k < j)
  • m[i,i] = 0 (边界条件)

Catalan数:n个矩阵的乘法次序数为第n个Catalan数

  • C(n) = (1/(n+1)) × C(2n, n)
  • 蛮力枚举复杂度 = Ω(4ⁿ / n

递归算法:直接递归,子问题大量重复计算,复杂度指数级

动态规划算法(非递归):

  • 按链长 r = 2 → n 逐步计算
  • 时间复杂度:O(n³)
  • 空间复杂度:O(n²)
  • 标记函数 s[i,j] 记录最优划分位置 k

3.2 投资问题

问题:m元钱,n项投资,fᵢ(x) 表示将x元投入第i项的效益,求总效益最大的分配方案。

递推方程:

  • Fₖ(x) = x元钱投给前k个项目的最大效益
  • Fₖ(x) = max { fₖ(xₖ) + Fₖ₋₁(x - xₖ) } (0 ≤ xₖ ≤ x)
  • F₁(x) = f₁(x) (边界)

复杂度:

  • 时间:O(n × m²)(每项投资尝试0到x的所有分配)
  • 空间:O(n × m)

3.3 背包问题(0-1背包)

问题:n种物品,第j种重量wⱼ、价值vⱼ,背包最大重量b,求最大价值。

递推方程:

  • Fₖ(y) = 只装前k种物品、总重不超过y时的最大价值
  • Fₖ(y) = max{ Fₖ₋₁(y), Fₖ₋₁(y - wₖ) + vₖ } (选或不选第k种)
  • F₀(y) = 0,Fₖ(0) = 0 (边界)

标记函数:i(k,y) 记录 Fₖ(y) 对应的最后一件物品编号

  • 若 Fₖ₋₁(y) ≥ Fₖ(y - wₖ) + vₖ,则 i(k,y) = i(k-1,y)
  • 否则 i(k,y) = k

回溯构造解:从 i(n,b) 开始反向追踪每件物品是否被选。

复杂度:

  • 时间:O(n × b)
  • 空间:O(n × b)(可优化至 O(b))
  • 注:b是数值输入,若b很大则为伪多项式时间

3.4 最长公共子序列(LCS)

定义:X的子序列Z = 从X中删除若干元素(可不连续)后得到的序列

问题:给定 X = <x₁…xₘ>,Y = <y₁…yₙ>,求最长公共子序列。

关键定理:设Z是X和Y的LCS

  • 若 xₘ = yₙ,则 zₖ = xₘ = yₙ,且 Zₖ₋₁ 是 Xₘ₋₁ 与 Yₙ₋₁ 的LCS
  • 若 xₘ ≠ yₙ,则 Z 是 Xₘ₋₁ 与 Y 的LCS 或 X 与 Yₙ₋₁ 的LCS

递推方程:

  • C[i,j] = Xᵢ 与 Yⱼ 的LCS长度
  • C[i,j] = 0,若 i=0 或 j=0
  • C[i,j] = C[i-1,j-1] + 1,若 xᵢ = yⱼ
  • C[i,j] = max(C[i-1,j], C[i,j-1]),若 xᵢ ≠ yⱼ

算法:

  • 填表:O(m × n) 时间,O(m × n) 空间
  • 回溯:用 B[i,j] 标记箭头方向(↖ ↑ ←),从(m,n)回溯到(0,0)

3.5 图像压缩

问题:像素灰度序列 {p₁…pₙ},灰度值 0~255(8位)。变位压缩:将序列分成m段,每段内用相同位数表示,段头附加11位(段长+位数),求总存储位数最小的分段方案。

递推方程:

  • s[i] = 前i个像素的最优分段存储位数
  • s[i] = min { s[i-k] + k × bmax(i-k+1, i) + 11 } (1 ≤ k ≤ min(i, 256))
  • s[0] = 0 (边界)
  • 每段最多256个像素(因为段长用8位表示)

复杂度:时间 O(n)(每步最多256次比较,常数级),空间 O(n)

3.6 最大子段和

问题:n个整数(可负)的序列,求 Σa[k](i≤k≤j)的最大值。

三种解法对比:

方法时间复杂度说明
蛮力枚举O(n³)枚举所有起点终点再求和
分治法O(n log n)左段/右段/跨中段,取最大值
动态规划O(n)在线算法,b[j]记录以j结尾的最大子段和

动态规划递推:

  • b[j] = 以第j个元素结尾的最大子段和
  • b[j] = max{ b[j-1] + a[j], a[j] }
  • 最终答案 = max{ b[j] } (1 ≤ j ≤ n)

算法(Kadane算法):

sum = 0; b = 0
for i = 1 to n:
    if b > 0:
        b = b + a[i]
    else:
        b = a[i]
    if b > sum:
        sum = b
return sum

时间 O(n),空间 O(1),动态规划中空间优化的极致。

3.7 凸多边形的三角划分

问题:凸n边形用n-3条内部不交对角线分成三角形,权函数W(三角形),求总权最小的划分。

递推方程:

  • t[i,j] = 顶点 vᵢ₋₁, vᵢ, …, vⱼ 构成的凸多边形的最优三角划分权值
  • t[i,j] = min { t[i,k] + t[k+1,j] + W(vᵢ₋₁, vₖ, vⱼ) } (i ≤ k < j)
  • t[i,i] = 0 (边界,退化为线段)

与矩阵链乘法同构:形式完全一致,都是区间DP,时间 O(n³)

3.8 电路布线

问题:n条导线,上端点1…n从左到右排列,下端点π(1)…π(n)是一个排列。导线(i, π(i)),求最大不相交导线子集。

相交条件:i < j 且 π(i) > π(j) → 两条导线相交

递推方程:

  • Size(i,j) = 上端点≤i、下端点≤j 的最大不相交导线数
  • 若 j < π(i):Size(i,j) = Size(i-1, j) (第i条线不在集合中)
  • 若 j ≥ π(i):Size(i,j) = max{ Size(i-1,j), 1 + Size(i-1, π(i)-1) }

本质:等价于求排列 π 的最长递增子序列(LIS)——LIS 就是一组互不相交的导线。

复杂度:时间 O(n²)(可优化至 O(n log n))

3.9 最优二叉搜索树

问题:n个排序元素 x₁<…<xₙ,存取概率 b₁…bₙ(命中)+ a₀…aₙ(未命中空隙),构造平均比较次数最小的二叉搜索树。

关键概念:

  • 实结点:x₁…xₙ(存储数据,命中)
  • 空隙结点:(-∞,x₁), (x₁,x₂), …, (xₙ,+∞)(未命中,叶结点)
  • 平均比较次数 = Σbᵢ × depth(xᵢ) + Σaⱼ × depth(空隙j)

递推方程:

  • m[i,j] = S[i,j] = {xᵢ…xⱼ} 对应的最优BST平均比较次数
  • w[i,j] = aᵢ₋₁ + bᵢ + aᵢ + … + bⱼ + aⱼ (子树概率和)
  • m[i,j] = min { m[i,k-1] + m[k+1,j] + w[i,j] } (i ≤ k ≤ j)
  • m[i,i-1] = 0 (边界)

复杂度:时间 O(n³),空间 O(n²)

  • 利用Knuth优化可降至 O(n²):根的选择范围随区间单调

3.10 RNA二级结构预测

问题:RNA序列 A,U,C,G 长度n,求最大碱基配对数的二级结构。

配对规则:

  • A-U,C-G(Watson-Crick配对)
  • 配对的碱基至少相隔4个位置:若(i,j)配对,则 i < j-4
  • 每个碱基最多参与一对
  • 无尖的转弯(没有伪结/pseudoknot)

递推方程:

  • S[i,j] = 碱基序列 i…j 的最大配对数
  • 若 j - i ≤ 4:S[i,j] = 0
  • 否则两种情况:
    • j不配对:S[i,j] = S[i, j-1]
    • j与k配对(i ≤ k ≤ j-5):S[i,j] = max { 1 + S[i, k-1] + S[k+1, j-1] }

复杂度:时间 O(n³),空间 O(n²)


四、动态规划问题的共同特征

4.1 适用条件

  1. 优化原则(最优子结构)——必要条件,不满足则绝对不能用
  2. 子问题重叠——不是必要条件,但这是用DP比递归快的原因
  3. 无后效性——当前决策只依赖当前状态,不依赖未来决策

4.2 常见DP模型分类

模型典型问题状态维度
线性DP最大子段和、LIS1维
区间DP矩阵链乘法、凸多边形三角划分、最优BST2维(区间起止)
背包DP0-1背包、投资问题2维(物品数×容量)
序列DPLCS、编辑距离2维(两序列下标)
树形DP树的最大独立集节点+选/不选
状态压缩DPTSP、集合问题位掩码集合

4.3 复杂度分析要点

  • 时间复杂度 = 状态数 × 每个状态的决策数
  • 空间复杂度 = 状态数(可滚动数组优化)
  • 伪多项式时间:输入是数值(如背包容量b),实际随输入规模指数增长

五、设计要点总结

  1. 状态定义是关键——状态定义决定了递推方程是否简洁可解
  2. 从子问题到原问题——自底向上,从小规模逐步扩大
  3. 标记函数不要忘——求最优值容易,构造最优解需要回溯
  4. 空间优化常可能——很多问题只需保留前一行/前一列
  5. 验证优化原则——不满足优化原则的问题不能用DP

与其他知识的关联