算法导论读书笔记(15)动态规划

第四部分 高级设计和分析技术
1. 动态规则通常用来解决最优化问题
2. 贪心算法通常用于最优化问题,其思想是每步选择都追求局部最优。其速度比动态规划方法快得多,但是,我们并不总能简单地判断出贪心算法是否有效
3. 摊还分析并不是通过分别分析每个操作的实际代价的界来分析操作序列的代价的界,而是直接分析序列整体的实际代价的界,好处是,虽然某些操作的代价可能很高,但其他很多操作的代价可能很价

第15章 动态规划

分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。
动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子问题。也就是不同的子问题会涉及相同的子子问题。这样,普通的递归方法会反复的求解那些公共子问题,因而浪费了时间,动态规划则是对公共子问题只求解一次,然后将其解保存在表格中,避免了不必要的重复工作

按如下4个步骤来设计一个动态规划算法:
1. 刻画一个最优解的结构特征
2. 递归地定义最优解的值
3. 计算最优解的值,通常采用自底向上的方法
4. 利用计算出的信息构造一个最优解

1~3是动态规划算法求解问题的基础,如果仅需要一个最优解的值,可以忽略步骤4。如果需要求得一个最优解,则有时要在第3步的计算中记录一些附加信息,以便用来构造一个最优解。

1. 钢条切割

钢条切割问题

这里写图片描述
长度为n英寸的钢条共有2^(n-1)种不同的切割方案。
这里写图片描述
在该问题中,对于最优解 , 可以用更短的钢条的最优解来描述:将钢条从左边切割下长度为i的一段,只对剩下的n-i的一段进行继续切割(递归求解),而不对左边长度为i的一段在进行切割
为了求解规模为n的原问题,可以先求解形式完全一样,但规模更小的子问题,这样,钢条切割问题满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。

自顶向下递归实现

Cut-Rod(p, n)
    if n == 0
        return 0
    q = -∞
    for i = 1 to n
        q = max(q, p[i] + Cut-Rod(p, n - i))
    return q

CUT-ROD的效率低是因为它反复求解相同的子问题

使用动态规划方法求解最优钢条切割问题

朴素递归算法效率很低,因为它反复求解相同的子问题。动态规划方法仔细安排求解,对每个子问题只求解一次,并将结果保存下来。随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算
动态规划方法是付出额外的内在空间来节省计算时间,是典型的时空权衡的例子。

动态规划有两种等价的实现方法:
1. 带备忘录自顶向下

Memoized-Cut-Rod(p, n)
    let r[0..n] be a new array
    for i = 0 to n
        r[i] = -∞
    Memoized-Cut-Rod-Aux(p, n , r)

Memoized-Cut-Rod-Aux(p, n, r)
    if r[n] >= 0
        return r[n]
    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
    return q
  1. 自底向上法
Bottom-Up-Cut-Rod(p, n)
    let r[0..n] be a new array
    r[0] = 0
    for j = 1 to n
        q = -∞
        for i = 1 to j
            q = max(q, p[i] + r[j-1])
        r[j] = q
    return r[n]

子问题图

当思考一个动态规划问题时,我们应该弄清所涉及的子问题及子问题之间的依赖关系。
这里写图片描述

重构解

扩展动态规划算法,使之对每个子问题不仅保存最优收益值,还保存对应的切割方案。

Extended-Bottom-Up-Cut-Rod(p, n)
    let r[0..n] and s[0..n] be a new array
    r[0] = 0
    for j = 1 to n
        q = -∞
        for i = 1 to j
            if q < p[i] + r[j-i]
                q = p[i] + r[j-i]
                s[j] = i
        r[j] = q
    return r and s

Print-Cut-Rod-Solution(p, n)
    (r, s) = Extended-Bottom-Up-Cut-Rod(p, n)
    while n > 0
        print s[n]
        n = n - s[n]

2. 矩阵链乘法

两个矩阵A和B只有相容,即A的列数等于B的行数时,才相相乘如果A是p q的矩阵,B是q * r的矩阵,那么乘积C是p * r的矩阵。计算C所需的乘法次数为p * q * r*

矩阵链乘法

给定n 个矩阵的序列,希望求它们的乘积: (A1,A2,A3,…….An) 。因为矩阵的乘法满足结合律,所以可以对n个矩阵序列加括号,来改变乘积顺序

不同的加括号的方案,对于乘积运算的代价影响很大,两个矩阵相乘,A为p q矩阵,B为q * r矩阵。所以A 的乘法次数为p * q * r 。如果A(10 ),B(100 ), C(5 )三个矩阵相乘,如果按照((AB)C)的顺序,则需要10*100*5 + 10*5*50 = 7500次乘法运算,如果按照(A(BC))的顺序,则需要100*5*50 + 10*100*50 = 75000次乘法运算。所以,不同的加括号方案,对于矩阵链乘法的代价影响很大。

矩阵链乘法问题: 给定n个矩阵的链(A1, A2, …,An),矩阵Ai的规模(维数)为pi-1 * pi(1<=i<=n)。求加括号方案,使得所需要的乘法运算次数最少

1. 刻画一个最优解的结构特征

动态规划方法的第一步是寻找最优子结构,然后就利用这种子结构从子问题的最优解中构造原问题的最优解。假设Ai…j表示矩阵AiAi+1Ai+2….Aj的结果。为了对AiAi+1Ai+2….Aj进行加括号,需要找到一个k,首先计算Ai..k然后计算Ak+1..j,然后计算他们的乘积,最终求得Ai…j。这个方案的代价是求Ai..k的代价,加上求Ak+1..j的代价,然后再加上Ai..k * Ak+1..j的代价。
如果在AiAi+1Ai+2….Aj的最优加括号方案,分割点为k,那么AiAi+1Ai+2….Ak同样也必须是最优的加括号方案,同理,Ak+1Ak+2….Aj也必须有最优的加括号方案。所以,该问题同样具有最优子结构性质。任何最优解都是由子问题的最优解构成的。所以,为了求得原问题的最优解,可以先求出子问题的最优解,也就是需要先找到分割点k,在确定k时,必须保证考察了所有的分割点。

2. 一个递归求解方案

令m[i,j]表示计算Ai…j的乘法运算次数的最小值,所以,对于i = 1,2,…,n。m[i,i] = 0。现在需要来递归定义m[i,j],分两种情况进行讨论如下:
a. 当i==j时:m[i,j] = 0,(此时只包含一个矩阵)
b. 当i < j 时:从步骤1中需要寻找一个k(i≤k<j)值,使得m[i,j] = min{ m[i,k] + m[k+1,j] + Pi-1PkPj } (i≤k<j)。

3. 计算最优代价

虽然给出了递归解的过程,但是在实现的时候不采用递归实现,而是借助辅助空间,使用自底向上的表格进行实现。

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
        m[i,i] = 0
    for l = 2 to n              // l is the chain length
        for i = 1 to n - l + 1
            j = i + l - 1
            m[i,j] = ∞
            for k = i to j - 1
                q = m[i,k] + m[k+1, j]+Pi-1PkPj
                if q < m[i,j]
                    m[i,j] = q
                    s[i,j] = k
    return m and s
4. 构造最优解

s记录了求 过程中,最优解的分界点k的信息,所以,利用s可以得到最优解:

Print-Optimal-Parens(s, i, j)
    if i == j
        print "A"i
    else
        print "("
        Print-Optimal-Parens(s, i, s[i,j])
        Print-Optimal-Parens(s, s[i,j]+1, j)
        print ")"

3. 动态规划原理

适合应用动态规划方法求解的最优化问题应该具备的两个要求最优子结构子问题重叠

最优子结构

动态规划方法求解的第一步就是刻画最优解的结构。如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质

动态规划方法中,我们通常自底向上地使用最优子结构。也就是说,首先求得子问题的最优解然后求原问题的最优解

在尝试使用动态规划方法时要小心,要注意问题是否具有最优子结构性质

重叠子问题

适用动态规划方法的第二个性质是子问题空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。如果递归算法反复求解相同的子问题,我们就称最优化问题具有重叠子问题

重构最优解

从实际考虑,我们通常将每个子问题所做的选择存在一个表中,这样就不必根据代价值来重构这些信息。

为详细说明重叠子问题性质,重新考察矩阵链乘法问题,在求解高层的子问题时,会反复查找低层上子问题的解,而不是简单地查表,那么运行时间会急剧上升

Recursive-Matrix-Chain(p, i, j)
    if i == j
        return 0
    m[i,j] = ∞
    for k = i to j = 1
        q = Recursive-Matrix-Chain(p, i, k) + 
            Recursive-Matrix-Chain(p, k+1, j) + Pi-1PkPj
        if q < m[i,j]
            m[i,j] = q
    return m[i,j]

如:Recursive-Matrix-Chain(p, 1, 4)产生的递归调用树。
这里写图片描述

备忘

自底向上动态规划算法会比自顶向下备忘算法快,因为自底向上算法没有递归调用的开销

Memoized-Matrix-Chain(p)
    n = p.length - 1
    let m[1..n, 1..n] be a new table
    for i = 1 to n
        for j = i to n
            m[i,j] = ∞
    return LookUp-Chain(m, p, 1, n)

LookUp-Chain(m, p, i, j)
    if m[i,j] < ∞
        return m[i,j]
    if i == j
        m[i,j] = 0
    else
        for k = i to j - 1
            q = LookUp-Chain(m, p, i, k) +
                LookUp-Chain(m, p, k+1, j) + Pi-1PkPj
            if q < m[i,j]
                m[i,j] = q
    return m[i,j]

4. 最长公共子序列

给定两个序列X和Y,如果Z既是X的子序列,也是Y的子序列,我们称它是X和Y的公共子序列。例如:X=(A, B, C, B, D, A, B),Y = (B, D, C, A, B, A),那么序列(B, C, A)就是X和Y的公共子序列,但不是最长公共子序列,因为它的长度为3,而(B, C, B, A)的长度为4是X和Y的最长公共子序列。
这里写图片描述

LCS-Length(X, Y)
    m = X.length
    n = Y.length
    let b[1..m,1..n] and c[0..m,0..n] be new tables
    for i = 1 to m
        c[i,0] = 0
    for j = 0 to n
        c[0,j] = 0
    for i = 1 to m
        for j = 1 to n
            if xi == yi
                c[i,j] = c[i-1,j-1] + 1
                b[i,j] = "↖"
            else if c[i-1,j] >= c[i,j-1]
                c[i,j] = c[i-1,j]
                b[i,j] = "↑"
            else
                c[i,j] = c[i,j-1]
                b[i,j] = "←"
    return c and b

Print-LCS(b, X, i ,j)
    if i == j
        return
    if b[i,j] == "↖"
        Print-LCS(b, X, i-1 ,j-1)
        printf xi
    else if b[i,j] == "↑"
        Print-LCS(b, X, i-1 ,j)
    else
        Print-LCS(b, X, i ,j-1)

这里写图片描述

5. 最优二叉搜索树

  1. 对于一个给定的概率集合,构造一棵期望搜索代价最小的二叉搜索树,称之为最优二叉搜索树;
  2. 最优二叉搜索树不一定是高度最矮的,概率最高的关键字也不一定出现在二叉搜索树的根结点;

这个问题称为最优二叉搜索树(optimal binary search tree)问题。其形式化定义如下:给定一个n个不同关键字已排序的序列K = < k1,k2,…,kn>(因此k1 < k2<…< kn),我们希望用这些关键字构造一棵二叉搜索树。对每个关键字ki,都有一个概率pi表示其搜索频率。有些要搜索的值可能不在K中,因此我们还与n+1个“伪关键字”d0,d1,d2…dn表示不在K中的值。d0表示所有小于k1的值,dn表示所有大于kn的值,对i=1,2,…,n-1,伪关键字di表示所有在ki和ki+1之间的值。对每个伪关键字di,也都有一个概率pi表示对应的搜索频率。
下图显示了对一个n=5个关键字的集合构造的两颗二叉搜索树。每个关键字ki是一个内部结点,而每个伪关键字di是一个叶结点。每次搜索要么成功(找到某个关键字ki)要么失败(找到某个伪关键字di),因此有如下公式:
这里写图片描述
由于我们知道每个关键字和伪关键字的搜索频率,因而可以确定在一棵给定的二叉搜索树T中进行一次搜索的期望代价。假定一次搜索的代价等于访问的结点数,即此次搜索找到的结点在T中的深度再加1。那么在T中进行依次搜索的期望代价为:
这里写图片描述
其中depth(T)表示一个结点在树T中的深度。最后一个等式是由公式(15.10)推导而来。在图15-9(a)中,我们逐结点计算期望搜索代价:
对于一个给定的概率集合,我们希望构造一棵期望搜索代价最小的二叉搜索树,我们称之为最优二叉搜索树。图15-9(b)所示的二叉搜索树就是给定概率集合的最优二叉搜索树,其期望代价为2.75。这个例子显示二叉搜索树不一定是高度最矮的。而且,概率最高的关键字也不一定出现在二叉树搜索树的根结点。在此例中,关键字k5的搜索概率最高,但最优二叉搜索树的根结点为k2(在所有以k5为根的二叉搜索树中,期望搜索代价最小者为2.85)。

与矩阵链乘法问题相似,对本问题来说,穷举并检查所有可能的二叉搜索树不是一个高效的算法,需要指数时间。所以,这里使用动态规划方法求解此问题。
步骤1:最优二叉搜索树的结构
为了刻画最优二叉搜索树的结构,我们从观察子树特征开始。考虑一棵二叉搜索树的任意子树。它必须包含连续关键字k(i),…k(j),1<=i<=j<=n,而且其叶结点必然是伪关键字d(i-1),…,d(i)。
我们现在可以给出二叉搜索树问题的最优子结构:如果一棵最优二叉搜索树T有一棵包含关键字k(i),…,k(j)的子树T’,那么T’必然是包含关键字k(i),…,k(j)和伪关键字d(i-1),…,d(j)的子问题的最优解。我们依旧用“剪切-粘贴”法来证明这一结论。如果存在子树T”,其期望搜索代价比T’低,那么我们将T’从T中删除,将T”粘贴到相应的位置,从而得到一颗期望搜索代价低于T的二叉搜索树,与T最优的假设矛盾。
我们需要利用最优子结构性质来证明,我们可以用子问题的最优解构造原问题的最优解。给定关键字序列k(i),…,k(j),其中某个关键字,比如说k(r)(i<=r<=j),是这些关键字的最优子树的根结点。那么k(r)的左子树就包含关键字k(i),…,k(r-1)(和伪关键字d(i-1),…,d(r-1) ),而有字数包含关键字k(r+1),…,k(j)(和伪关键字d(r),…,d(j) )。只要我们检查所有可能的根结点k(r)(i<=r<=j),并对每种情况分别求解包含k(i),…,k(r-1)及包含k(r+1),…,k(j)的最优二叉搜索树,即可保存找到原问题的最优解。
这里还有一个值得注意的细节——“空子树”。假定对于包含关键字ki,…,kj的子问题,我们选定ki为根结点。根据前文论证,k(i)的左子树包含关键字k(i),…,k(i-1)的子问题,我们将此序列解释为不包含任何关键字。但请注意,子树仍然包含伪关键字。按照惯例,我们认为包含关键字序列k(i),…,k(i-1)的子树不包含任何实际关键字,但包含单一伪关键字d(i-1)。对称地,我们如果现在k(j)为根结点,那么k(j)的右子树包含关键字k(j+1),…,k(j)——此右子树不包含任何实际关键字,但包含伪关键字d(j)。
步骤2:一个递归算法
我们已经准备好给出最优解值的递归定义。我们选取子问题域为:求解包含关键字k(i),…,k(j)的最优二叉搜索树,其中i>=1,j<=n且j>=i-1(当j=i-1时,子树不包含实际关键字,只包含伪关键字d(i-1)。定义e[i,j]为包含关键字k(i),…,k(j)的最优二叉搜索树中进行一次搜索的期望代价,最终,我们希望计算出e[1,n]。
j=i-1的情况最为简单,由于子树只包含伪关键字d(i-1),期望搜索代价为e[i,i-1]=q(i-1)。
当j>=i时,我们需要从k(i),…,k(j)中选择一个跟结点k(r),然后构造一棵包含关键字k(i),…,k(r-1)的最优二叉搜索树作为其左子树,以及一棵包含关键字k(r+1),…,k(j)的二叉搜索树作为其右子树。当一棵子树成为一个结点的子树时,期望搜索代价有何变化?由于每个结点的深度都增加了1,根据公式(15.11),这棵子树的期望搜索代价的增加值应为所有概率这和。对于包含关键字k(i),…,k(j)的子树,所有概率之和为
因此,若k为包含关键字k(i),…,k(j)的最优二叉搜索树的根结点,我们有如下公式:
e[i,j]=p(r)+(e[i,r-1]+w(i,r-1))+(e[r+1,j]+w(r+1,j)) 注意,w(i,j)=w(i,r-1)+p(r)+w(r+1,j)。
因此e[i,j]可重写为:
e[i,j]=e[i,r-1]+e[r+1,j]+w(i,j)。 (15.13)
递归公式(15.13)假定我们知道哪个结点k应该作为根结点。如果选取期望搜索代价最低者作为根结点,可得最终递归公式(15.14):
①若j=i-1,e[i,j]=q(i-1)
②若i<=j,e[i,j]=min{e[i,r-1]+e[r+1,j]+w(i,j)}(i<=r<=j)
e[i,j]的值给出了最优二叉搜索树的期望搜索代价。为了记录最优二叉搜索树的结构,对于包含关键字k(i),…,k(j)(1<=i<=j<=n)的最优二叉搜索树,我们定义root[i,j]保存根结点k(r)的下标r。
步骤3:计算最优二叉搜索树的期望搜索代价
现在,我们可以注意到我们求解最优二叉搜索树和矩阵链乘法的一些相似之处。它们的子问题都由连续的下标子域组成。而公式(15.14)的直接递归实现,也会与矩阵链乘法问题的直接递归算法一样低效。因此,我们设计替代的高效算法,我们用一个表e[1..n+1,0..n]来保存e[i,j]的值。第一维下标上界为n+1而不是n,原因在于对于只包含伪关键字d(n)的子树,我们需要计算并保存e[n+1,n]。第二维下标下界为0,是因为对于只包含伪关键字d0的子树,我们需要计算并保存e[1,0]。我们只使用表中满足j>=i-1的表项e[i,j]。我们还使用一个表root记录关键字ki,…kj的子树的根。我们只使用此表中满足1<=i<=j<=n的表项root[i,j];
我们还需要另一个表来提高计算效率。为了避免每次计算e[i,j]时都重新计算w(i,j),我们将这些值保存在表w[1..n+1,0..n]中,这样每次可节省Θ(j-i)次加法。对基本情况,令w[i,i-1]=q(i-1)(1<=i<=n+1)。对j>=i的情况,可如下计算:
w[i,j]=w[i,j-1]+p(j)+q(j) (15.15)
这样对Θ(n^2)个w[i,j],每个计算时间为Θ(1)。下面的代码接受概率列表p和q及规模n作为输入,返回表e和root。

Optimal-Bst(p, q, n)
    let e[1..n+1, 0..n], w[1..n, 0..n], and root[1..n, 1..n] be new tables
    for i = 1 to n + 1
        e[i,i-1] = qi - 1
        w[i,i-1] = qi - 1
    for l = 1 to n
        for i = 1 to n -l + 1
            j = i + l - 1
            e[i,j] = ∞
            w[i,j] = w[i,j-1] + pj + qj
            for r = i to j
                t = e[i,r-1] + e[r+1,j] + w[i,j]
                if t < e[i,j]
                    e[i,j] = t
                    root[i,j] = r
    return e and root
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值