动态规划方法通常用来求解最优化问题(optimization problem)。

我们通常按如下4个步骤来设计一个动态规划算法:

动态规划算法求解问题的基础。

  1. 刻画一个最优解的结构特征
  2. 递归地定义最优解的值
  3. 计算最优解的值,通常采用自底向上的方法

维护一些额外信息,以便用来构造一个最优解

  1. 利用计算出的信息构造一个最优解

15.1 钢条切割

研究如何将长钢条切割成短钢条,使得总价值最高

钢条切割问题

给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,……,n),求切割钢条方案,使得销售收益rn最大。注意,如果长度为n英寸的钢铁的价格pn足够大,最优解可能就是完全不需要切割。

最优子结构性质

问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

伪代码 自顶向下递归实现
//p:价格数组
//n:整数
//返回值:长度为n的钢条的最大收益
CUT-ROD(p,n)
  if n == 0
    return 0
  q = -∞//将最大收益q初始化为-∞
  for i = 1 to n
    q = max(q,p[i] + CUT-ROD(p,n-1))
  return q//返回计算结果
伪代码 带备忘的自顶向下法动态规划
//p:价格数组
//n:整数
//返回值:长度为n的钢条的最大收益
MEMOIZED-CUT-ROD(p,n)
  let r[0……n] be a new array
  for i = 0 to n
    r[i]= -∞
  return MEMOIZED-CUT-ROD-AUX(p,n,r)

MEMOIZED-CUT-ROD-AUX(p,n,r)
  if r[n] ≥ 0//检查所需值是否已知
    return r[n]//如果是,则直接返回保存的值
    //用通常方法计算所需值q【14-18】
  if n == 0
    q = 0
  else q = - ∞
    for i = 1 to n
      q = max(q,p[i] + MEMOIZED-CUT-ROD-AUX(p,n-i,r))
  r[n] = q//将q存入r[n]
  return q//将q返回
伪代码 自底向上法动态规划
BOTTOM-UP-CUT-ROD(p,n)
  let r[0……n] be a new array//创建一个新数组r[0……n]来保存子问题的解
  r[0] = 0//将r[0]初始化为0,因为长度为0的钢条没有收益
  //[5-8]对j=1,2,……,n按升序求解每个规模为j的子问题,求解规模为j的子问题的方法与CUT-ROD所采用的方法相同
  for j = 1 to n
    q = -∞
    for i = 1 to j
      q = max (q,p[i] + r[j-i])//现在直接访问数组元素r[j-i]来获得规模为j-i的子问题的解,而不必进行递归调用
    r[j]=q//将规模为j的子问题的解存入r[j]
  return r[n]//返回r[n],即最优解rn

15.1-3

alt

// n:钢条长度
// p:价格表p[1……n]
// c:切割成本
//返回值:长度为n的钢条的最大收益
BOTTOM-TOP-CUT-ROD(p,c,n)
  create a new array r[0……n]
  r[0] = 0
  for i = 1 to n
    q= p[i]
    for j = i - 1 to 1
      q = max(q,p[j] + r[i-j] - c)
    r[i] = q
  return r[n]

15.1-5

alt

//n:斐波那契数列的元素的索引号
//返回值:第n个斐波那契数列的值
FIBONACII(n)
  if n < 2
    return 1
  else 
    f1 = 1
    f2 = 1
    for i = 2 to n
      q = f1 + f2
      f2 = f1
      f1 = q
    return q

该算法的核心是从2到n的循环迭代,这反映了自下而上的求解方法。

每次迭代 i,只需要用到前两个元素,即第 i-1 个元素和第 i-2 个元素。

因此,仅需要用两个局部变量f1和f2来保存第 i-1 个元素和第 i-2 个元素,

并且每次迭代后更新这两个局部变量,而无需保存从0到 n 的每个元素。

显然,这个算法的时间复杂度为O(n)。

alt

15.2 矩阵链乘法

解决如何用最少的标量乘法操作完成一个矩阵链相乘的运算

伪代码 两个矩阵相乘的标准算法
//rows:矩阵的行数
//columns:矩阵的列数
MATRIX-MULTIPLY(A,B)
  if A.columns ≠ B.rows
    errror "incompatible dimensions【不相容维数】"
  else let C be a new A.rows × B.columns matrix
    for i = 1 to A.rows
      for j = 1 to B.columns
        c[ij] = 0
        for k = 1 to A.columns
          c[ij] = c[ij] + a[ik] · b[kj]
  return C

两个矩阵A和B只有相容【compatible】,即A的列数等于B的行数时才能相乘。

如果A是p×q的矩阵,B是q×r的矩阵,那么乘积C是p×r的矩阵。

计算C所需时间由第11行的标量乘法的次数决定,即pqr。

矩阵链乘法问题

给定n个矩阵的链<A1,A2,……An>,矩阵Ai的规模为p^(i-1)×pi(1 ≤ i ≤ n),求完全括号化方法,使得计算乘积A1A2……An所需标量乘法次数最少

步骤1:最优括号化方案的结构特征

步骤2:一个递归求解方案

步骤3:计算最优代价

伪代码
//矩阵Ai的规模为p^(i-1)×p^(i)(i=1,2,……,n)
//序列p=<p0,p1,……,pn>
//m[i,j]表示计算矩阵A[i,j]所需标量乘法次数的最小值
//s[i,j]保存A[i]A[i+1]...A[j]最优括号化方案的分割点位置k,即使得m[i,j]=m[i,k]+m[k+1,j]+p[i-1]p[k]p[j]成立的k值
MATRIX-CHAIN-ORDER(p)
//求出了计算矩阵链乘积所需的最少标量乘法运算次数
//但并未直接指出如何进行这种最优代价的矩阵链乘法计算
  n = p.length - 1
  let m[1……n,1……n] and s[1……n-1,2……n] be new tables
  for i = 1 to n//对所有i=1,2,……,n计算
    m[i,i] = 0 // 长度为1的链的最小计算代价
  for l= 2 to n // l is the chain length
    j = i + l - 1
    m[i,j] = ∞
    for k = i to j-1
    q = m[i,k] + m[k+1,j] + p[i-1]p[k]p[j]
    if q < m[i,j]
      m[i,j] = q
      s[i,j] = k
  return m and s
执行算法的过程

alt alt

步骤4:构造最优解

伪代码 最优括号化方案
//s i j:MATRIX-CHAIN-ORDER得到的表s及下标i和j
//调用PRINT-OPTIMAL-PARENS(s,1,n)即可输出<A1,A2,……,An>的最优括号化方案
PRINT-OPTIMAL-PARENS(s,i,j)
  if i == j
    print "A"
  else print "("
    PRINT-OPTIMAL-PARENS(s,i,s[i,j])
    PRINT-OPTIMAL-PARENS(s,s[i,j]+1,j)
    print ")"

15.2-1

alt

最优括号化方案为((A1A2)((A3A4)(A5A6))),所需要的标量乘法的次数为2010

验证 python代码
def MATRIX_CHAIN_ORDER(p):
    n = len(p)
    s = [[0 for j in range(n)] for i in range(n)]
    m = [[0 for j in range(n)] for i in range(n)]
    for l in range(2, n):  # l is the chain length
        for i in range(1, n - l + 1):
            j = i + l - 1
            m[i][j] = 1e9
            for k in range(i, j):
                q = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j]
                if q < m[i][j]:
                    m[i][j] = q
                    s[i][j] = k
    print()
    PRINT_OPTIMAL_PARENS(s, 1, n - 1)
    print()

def PRINT_OPTIMAL_PARENS(s, i, j):
    if i == j:
        print('A', end='')
        print(i, end='')
    else:
        print('(', end='')
        PRINT_OPTIMAL_PARENS(s, i, s[i][j])
        PRINT_OPTIMAL_PARENS(s, s[i][j] + 1, j)
        print(')', end='')

A = [5, 10, 3, 12, 5, 50, 6]
MATRIX_CHAIN_ORDER(A)

15.4 最长公共子序列

展示如何用动态规划方法找到两个序列的最长公共子序列

子序列

alt alt

公共子序列

alt

最长公共子序列问题

alt

步骤1:刻画最长公共子序列的特征
定理 15.1 LCS的最优子结构

alt alt

步骤2:一个递归解

alt

alt

公式 15.9

alt

步骤3:计算LCS的长度
步骤4:构造LCS

15.4-5

alt

Solution

给出一个数字列表L,复制一个名为L'的L,然后对L'进行排序

伪代码 过程 PRINT-LCS(c, X, Y)
PRINT-LCS(c, X, Y)
    n = c[X.length, Y.length]
    let s[1..n] be a new array
    i = X.length
    j = Y.length
    while i > 0 and j > 0
        if x[i] == y[j]
            s[n] = x[i]
            n = n - 1
            i = i - 1
            j = j - 1
        else if c[i - 1, j] ≥ c[i, j - 1]
            i = i - 1
        else j = j - 1
    for i = 1 to s.length
        print s[i]
伪代码 过程 MEMO-LCS-LENGTH-AUX(X, Y, c, b)
MEMO-LCS-LENGTH-AUX(X, Y, c, b)
    m = |X|
    n = |Y|
    if c[m, n] != 0 or m == 0 or n == 0
        return
    if x[m] == y[n]
        b[m, n] = ↖
        c[m, n] = MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y[1..n - 1], c, b) + 1
    else if MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y, c, b) ≥ MEMO-LCS-LENGTH-AUX(X, Y[1..n - 1], c, b)
        b[m, n] = ↑
        c[m, n] = MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y, c, b)
    else
        b[m, n] = ←
        c[m, n] = MEMO-LCS-LENGTH-AUX(X, Y[1..n - 1], c, b)
伪代码 过程 MEMO-LCS-LENGTH(X, Y)
MEMO-LCS-LENGTH(X, Y)
    let c[1..|X|, 1..|Y|] and b[1..|X|, 1..|Y|] be new tables
    MEMO-LCS-LENGTH-AUX(X, Y, c, b)
    return c and b

然后,在这两个列表上运行theLCS算法。最长的公共子序列必须是单调递增的,因为它是L'的一个子序列,已排序。它也是最长的单调递增子序列,因为作为L'的子序列只会增加子序列必须是单调递增的限制。自从∣L∣=∣L′∣=n、 排序L可以在o(n^2)时间内完成,最终运行时间为o(|L||L’|)=o(n^2)。

15.4-6

Give an O(nlgn)-time algorithm to find the longest monotonically increasing subsequence of a sequence of n numbers. Hint: Observe that the last element of a candidate subsequence of length i is at least as large as the last element of a candidate subsequence of lengthi−1. Maintain candidate subsequences by linking them through the input sequence.)

给出了求n个数序列的最长单调递增子序列的O(nlgn)-时间算法。提示:注意长度为i的候选子序列的最后一个元素至少与长度为i的候选子序列的最后一个元素一样大−1.通过输入序列链接候选子序列来维护候选子序列。)

Solution

LONG-MONOTONIC(A)算法返回A的最长单调递增子序列,其中A的长度为n。

该算法的工作原理如下:将创建一个新数组B,使得B[i]包含长度为ii的最长单调递增子序列的最后一个值。一个新的数组C将是这样的,即C[i]包含长度ii的单调递增子序列,其中最后一个元素最小。

要分析运行时间,请注意B的条目是按排序顺序排列的,因此我们可以在O(lgn)时间内执行第9行。由于for循环中的每一行占用固定时间,因此总运行时间为O(nlgn)。

伪代码 过程 LONG-MONOTONIC(A)
LONG-MONOTONIC(A)
    let B[1..n] be a new array where every value = ∞
    let C[1..n] be a new array
    L = 1
    for i = 1 to n
        if A[i] < B[1]
            B[1] = A[i]
            C[1].head.key = A[i]
        else
            let j be the largest index of B such that B[j] < A[i]
            B[j + 1] = A[i]
            C[j + 1] = C[j]
            INSERT(C[j + 1], A[i])
            if j + 1 > L
                L = L + 1
    print C[L]