• 算法导论读书笔记(17)


    算法导论读书笔记(17)

    动态规划概述

    和分治法一样, 动态规划 (dynamic programming)是通过组合子问题的解而解决整个问题的。分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题。在这种情况下,分治法会重复地求解公共的子子问题。而动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免重复。

    动态规划通常用于 最优化问题 。此类问题可能有多种可行解。每个解有一个值,而我们希望找出具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因而可能存在多个最优解。

    动态规划算法的设计可以分为如下4个步骤:

    1. 描述最优解的结构
    2. 递归定义最优解的值
    3. 按自底向上的方式计算最优解的值
    4. 由计算出的结构构造一个最优解

    钢条切割

    钢条切割问题是动态规划问题的一个例子。塞林企业会买进长钢条,将它们切割成短条后卖出(切割是免费的,不计成本)。塞林企业的老总想要知道钢条怎么切割最赚钱。

    已知塞林企业对长度为 i 英寸的钢条的售价为 pi 美元,其中 i = 1,2,…。下图给出了一张样本价格表。

    钢条切割问题 的描述如下。给定一根长为 n 的钢条以及价格表 pi ,其中 i = 1,2,…, n ,找出钢条切割并卖出后可取得的最大收益 rn

    考虑一下 n = 4的情况。下图列出了切割4英寸钢条的所有方式。根据样本价格表,最后可知将4英寸钢条切割成2根2英寸钢条的收益最大。 p2 + p2 = 10。

    一根长度为 n 的钢条共有 2n-1 种不同的切割方式。我们这里用普通加法符号表示一个分解,比如7 = 2 + 2 + 3就表示一根长度为7的钢条被切成3份,2根长度为2,一根长度为3。如果最优解将钢条切割成 k 份( 1 <= k <= n ),那么最优分解即为 n = i1 + i2 + … + ik ,每段钢条的长度为 i1i2 ,…, ik ,对应的最大收益为 rn = pi1 + pi2 + … + pik

    一般来说,我们可以将最优收益 rn 表示成如下形式:

    rn = max ( pn , r1 + rn-1 , r2 + rn-2 ,…, rn-1 + r1 )

    第一个参数 pn 代表不切割时钢条的价格。其它的 n - 1个参数首先将钢条分为2份,长度分别为 in - ii = 1,2,…, n - 1),然后分别取得两份的最优收益 rirn-i 之后做和。因为我们不知道 i 取值为多少时会得到最优解,所以我们必须计算所有可能情况并从中选出最优解。

    可以看到,为了解决规模为 n 的初始问题,我们首先要解决的是规模小一些的同类型问题。一旦我们做出了一个划分,我们就可以将划分出的两部分视为钢条切割问题的独立的实例。整体最优解就包含在这相关的两部分子问题之中。我们说钢条切割问题满足 最优子结构 的性质:某问题的最优解由相关子问题的最优解组合而成,且这些子问题可以独立求解。

    下面以一种简单的方式安排钢条切割的递归结构,我们能看到一个分解是由位于左侧长度为 i 的一份,以及位于右侧的剩余部分 n - i 。只有右侧的部分可能再次被分解。这样可以得到一个更简洁的公式:


    在上面的公式中,最优解只和一个相关的子问题有关(划分后右侧的剩余部分)。

    自顶向下的递归实现

    下面的过程是一种很直接的,自顶向下,递归风格的实现。

    CUT-ROD(p, n)
    1 if n == 0
    2     return 0
    3 q = -∞
    4 for i = 1 to n
    5     q = max(q, p[i] + CUT-ROD(p, n - i))
    6 return q
    

    过程 CUT-ROD 接受一个价格的数组 p [ 1 .. n ]和一个整数 n 作为参数,返回可能的最优解。如果你用自己最熟悉的语言实现了这个 CUT-ROD 过程并运行它,你会发现即使对于不太大的 n 值,你的程序也会花很长的时间才能得出结果。实际上,每次你将 n 值增加1,你的程序的运行时间大约要翻一番。

    过程 CUT-ROD 的效率如此低下的原因就是它不断的重复解决相同的子问题。下图给出了一个很好的说明,其中 n = 4,可以看到,过程多次重复计算 n = 2和 n = 1。

    为了分析 CUT-ROD 的运行时间,设 T ( n )为问题规模为 n 时调用 CUT-ROD 的总次数,该表达式等于根结点标记为 n 的递归树中的总结点数。该总数包含根结点上的初始调用。因此, T ( 0 ) = 1和

    其中 j = n - i ,可得 T ( n ) = 2n ,因此 CUT-ROD 的运行时间是 n 的幂。

    使用动态规划解决钢条切割问题

    可以看到,递归算法之所以效率低下,是因为它反复求解相同的子问题。,因此,动态规划会仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来以便之后查找。由此可见,动态规划需要额外的内存空间来节省计算时间,是典型的 时空权衡 (time-memory trade-off)的例子。

    动态规划有两种等价的实现方法。

    带备忘的自顶向下法(top-down with memoization)
    此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解。
    自底向上法(bottom-up method)
    这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小”子问题的求解。因而我们可以将子问题按规模排序,由小到大一次求解。当求解某子问题时,它所依赖的那些更小子问题都已求解完毕,因此每个子问题只求解一次。

    两种方法得到的算法具有相同的渐进运行时间,但自底向上方法的时间函数通常具有更小的系数。

    下面给出的是自顶向下 CUT-ROD 过程的伪码,加入了备忘机制:

    MEMOIZED-CUT-ROD(p, n)
    1 let r[0..n] be a new array
    2 for i = 0 to n
    3     r[i] = -∞
    4 return MEMOIZED-CUT-ROD-AUX(p, n, r)
    
    MEMOIZED-CUT-ROD-AUX(p, n, r)
    1  if r[n] >= 0
    2      reutrn r[n]
    3  if n == 0
    4      q = 0
    5  else
    6      q = -∞
    7      for i = 1 to n
    8          q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n - i, r))
    9  r[n] = q
    10 return q
    

    过程 MEMOIZED-CUT-ROD 首先检查值是否已知,如果是,则返回;否则在第6~8行计算值 q ,第9行将 q 存入 r [ n ],最后返回 q

    自底向上版本更简单:

    BOTTOM-UP-CUT-ROD(p, n)
    1 let r[0..n] be a 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[n]
    

    过程 BOTTOM-UP-CUT-ROD 采用子问题的自然顺序:若 i < j ,则规模为 i 的子问题比规模为 j 的子问题“更小”。因此,过程依次求解规模为 j = 0,1,…, n 的子问题。

    子问题图

    当思考一个动态规划为问题时,我们应该了解问题的子问题之间的依赖关系。

    问题的 子问题图 准确地表达了这些信息,子问题图是一个有向图,每个定点唯一地对应一个子问题。如果求子问题 x 的最优解时需要直接用到子问题 y 的最优解,那么在子问题图中就会有一条从子问题 x 到子问题 y 的有向边。下图显示了 n = 4时钢条切割问题的子问题图。

    子问题图 G = ( V , E )的规模可以帮助我们确定动态规划的运行时间。由于每个子问题只求解一次,因此算法运行时间等于每个子问题求解时间之和。通常,一个子问题的求解时间与子问题图中对应顶点的度成正比,而子问题的数目等于子问题的顶点数。因此,通常情况下,动态规划算法的运行时间与顶点和边的数量呈线性关系。

    重构解

    上面的算法仅返回最优解的收益值,并未返回解本身。这里可以扩展该算法。

    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
    

    钢条切割问题的简单Java实现

    /**
     * 带备忘的自顶向下方法
     *
     * @param price 价格表
     * @param n     待分割的长度
     */
    public static int memoizedCutRod(int[] price, int n) {
        int[] revenue = new int[n + 1];
        for (int i = 0; i < revenue.length; i++)    // 初始化revenue数组
            revenue[i] = Integer.MIN_VALUE;
        return memoizedCutRodAux(price, n, revenue);
    }
    
    private static int memoizedCutRodAux(int[] price, int n, int[] revenue) {
        int q;
        if (revenue[n] >= 0)    // 如果revenue数组中有记录,就返回数组中的结果
            return revenue[n];
        if (n == 0)
            q = 0;
        else {
            q = Integer.MIN_VALUE;
            for (int i = 1; i <= n; i++)
                q = Integer.max(q, price[i] + memoizedCutRodAux(price, n - i, revenue));
            revenue[n] = q;
        }
        return q;
    }
    
    /**
     * 自底向上法
     *
     * @param price 价格表
     * @param n     待分割的长度
     */
    public static int bottomUpCutRod(int[] price, int n) {
        int[] revenue = new int[n + 1];
        int q;
        revenue[0] = 0;
        for (int j = 1; j <= n; j++) {
            q = Integer.MIN_VALUE;
            for (int i = 1; i <= j; i++)
                q = Integer.max(q, price[i] + revenue[j - i]);
            revenue[j] = q;
        }
        return revenue[n];
    }
    
  • 相关阅读:
    C/C++编译过程
    Struts2入门01
    NET CORE 微软官方说明链接
    PL/SQL控制语句(二、循环控制语句)
    PL/SQL控制语句(一、分支控制语句)
    PL/SQL数据类型
    PL/SQL变量的作用域和可见性
    PL/SQL变量和类型
    CopyWebpackPlugin 的使用
    flex
  • 原文地址:https://www.cnblogs.com/sungoshawk/p/3775288.html
Copyright © 2020-2023  润新知