15. 动态规划
动态规划和分治法相似,都是通过组合子问题的解来求解原问题的解。但不同的是分治法将问题划分为不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。相反,动态规划应用于子问题相互重叠的情况,即不同的子问题具有公共的子子问题。这种情况下,用分治法会反复的求解相同的子问题,造成时间浪费。动态规划对每个子问题只求解一次,并且保存在表格中,从而避免重复计算。
动态规划通常用于求解最优化问题。
我们通常按照如下步骤设计一个动态规划算法:
- 刻画一个最优解的结构特点
- 递归的定义最优解的值
- 计算最优解的值,通常采用自底而上的方法
- 利用计算出的信息构造一个最优解
如果只需要最优解的值,而非最优解本身,可忽略第四步。如果需要做第四步,往往需要在执行第三步时维护一些额外信息。
钢条切割
钢条切割问题:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2…n),求切割方案使得收益rn最大。
考虑当n=4时,所有的切割方案如下,我们发现当把4英寸的钢条切割为两段2英寸长的钢条时产生的收益最大。
当区分1+3切法和3+1切法时,n英寸的钢条有2n-1种切割方案。
我们可以手动得到ri,及对应切割方案:
对于rn,我们可以用更短的钢条的最优切割收益来描述它:
rn=max(pn,r1+rn−1,r2+rn−2,...,rn−1+r1). r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , . . . , r n − 1 + r 1 ) .
这时我们找到了钢条切割问题的最优子结构。每次切割后我们可以把切开的两段钢条看做独立的钢条切歌问题。一段钢条的最优解为所有切割方案中最大的。
我们可以将rn的式子简化为: rn=max1≤i≤n(pi+in−i) r n = max 1 ≤ i ≤ n ( p i + i n − i ) .
自顶而下的递归实现
根据上面的式子,我们可以得到下面这种自顶而下的递归实现(p为价格数组):
这种实现的递归式为: T(n)=1+∑j=0n−1T(j). T ( n ) = 1 + ∑ j = 0 n − 1 T ( j ) .
我们可以求得 T(n)=2i T ( n ) = 2 i ,这个时间是非常惊人的,而这么大的原因就是因为包含了非常多的重复计算。下图是递归树,红线圈起来的都是重复计算部分。
使用动态规划方法
- 带备忘的自顶而下法
与前面的递归方法类似,但是用了一个数组r[1..n]记录最优解,如果发现r[n]已经有记录,则无需重复计算,直接返回该值。
- 自底而上法
自底而上法采用子问题的自然顺序,依次求解规模为j=1,2…,n的子问题。
无论是带备忘的自顶而下法还是自底而上法,其中都是用了一个额外数组来存储子问题的最优解,从而避免重复计算子问题的解。动态规划付出额外的内存空间来节省时间,是典型的时空权衡的例子。
子问题图
当思考动态规划问题时,弄清所涉及子问题及子问题之间的依赖关系很关键。
问题的子问题图准确的表达了这些信息:
途中结点表示钢条切割的子问题规模,箭头表示子问题之间的依赖关系。
有了子问题图,我们便可轻易的得知自底而上的顺序应该如何。
而且子问题图G=(V, E)还可以帮助我们确定动态规划算法的运行时间。由于每个子问题只求解一次,因此算法运行时间等于每个子问题求解时间之和。通常,一个子问题的求解时间与子问题图中对应顶点的度(射出边的数目)成正比,而子问题的数目等于子问题图的顶点数。因此,通常情况下,动态规划算法的运行时间与顶点和边数量呈线性关系。
重构解
前面的方法只给出的如何求出最优解的值,但并未返回解本身。
下面是基于自底而上动态规划算法的扩展,其中用数组s[0..n]存储n英寸钢条的最优切割方案,s[i]存储的数字表示,i英寸的钢条切割为s[i]英寸和i-s[i]英寸时最优,其中s[i]英寸部分不再切割,i-s[i]英寸部分的最优解需要递归查看。
所以可以通过PRINT-CUT-ROD-SOLUTION来打印出n英寸钢条的一个最优切割方案。
矩阵链乘法
给定一个n个矩阵的序列(矩阵链) <A1,A2,...,An> < A 1 , A 2 , . . . , A n > <script type="math/tex" id="MathJax-Element-5"> </script>,我们希望求他们的乘积 A1A2