动态规划总结

一. 什么是动态规划(Dynamic Programming)?

1)动态规划是运筹学中用于解决策过程中的最优化数学方法。当然,我们在这里关注的是作为一种算法设计技术,作为一种使用多阶段决策过程最优的通用方法

2)如果问题是由交叠的子问题所构成,我们就可以用动态规划技术来解决它,一般来说,这样的子问题出现在对给定问题求解的递推关系中,这个递推关系包含了相同问题的更小子问题的解。动态规划法建议,与其对交叠子问题一次又一次的求解,不如把每个较小子问题只求解一次并把结果记录在表中(动态规划也是空间换时间的),这样就可以避免重复求解。


关键词:

1)它往往是解决最优化问题滴;

2)问题可以表现为多阶段决策(多阶段决策:一步步的决策,无后效性,决策只依赖于当前状态,不依赖于之前的状态);

3)交叠子问题:什么是交叠子问题,最优子结构性质。


动态规划的思想是什么:记忆,空间换时间,不重复求解,由交叠子问题从较小问题解逐步决策,构造较大问题的解。


一般来说,一个经典的动态规划算法时自底向上的(从较小问题的解,由交叠性质,逐步决策处较大问题的解),它需要解出给定问题的所有较小子问题。动态规划的一个变种是试图避免对不必要的子问题求解。如果采用自顶向下的递归来解,那么就避免了不必要子问题的求解(相对于动态规划表现出优势),然而递归又会导致对同一个子问题多次求解(相对于动态规划表现出劣势),所以将递归和动态规划结合起来,就可以设计一种基于记忆功能的自顶向下的动态规划算法

动态规划算法设计步骤:

1)找出最优解的结构,并刻画出其特征;

2)递归的定义最优解的值(最关键);

3)使用自底向上的方式计算出最优值;

4)根据计算最优值时得到的信息,构造最优解。

二. 实例分析——计算二项式系数

在排列组合里面,我们有下面的式子(很容易用组合的定义来证明):

    当 n>k>0 时,C(n , k) = C(n-1 , k -1) + C(n -1, k)

这个式子将C(n , k)的计算问题表述为了(问题描述)C(n-1 , k -1)和C(n -1, k)两个较小的交叠子问题。

初始条件:C(n , n) = C(n , 0) = 1。

我们可以用下列填矩阵的方式求出C(n , k):

该算法的时间复杂度是多少呢?可以大概的估计下,只填了下三角矩阵,为n*k/2  =  n*k。

矩阵怎么填(填矩阵的顺序)?

按行来填矩阵,算法伪代码:

算法伪代码

第1个for是控制行的,要填到第n行。第2个for来控制每行填到哪的,到i和k的较小值。从这2个for也可以看出复杂度是n*k。

实现:

View Code
 1 package cn.ac.iscas;
2
3 /*动态规划 计算二项式系数*/
4 public class BinoCoeff {
5
6 public static int Binomial(int n, int k) {
7 // 计算二项式系数C(n,k)
8 int[][] result = new int[n + 1][n + 1];
9 for (int i = 0; i <= n; i++) // 按行来填矩阵
10 {
11 for (int j = 0; j <= min(i, k); j++) // min(i,k)是这一行需要填的列数
12 {
13 if (j == 0 || j == i)
14 result[i][j] = 1;
15 else
16 result[i][j] = result[i - 1][j - 1] + result[i - 1][j];
17 }
18 }
19 return result[n][k];
20 }
21
22 private static int min(int i, int k) {
23 return i < k ? i : k;
24 }
25
26 public static void main(String[] args) {
27 System.out.println("输出8的二项式系数:");
28 for (int i = 0; i <= 8; i++)
29 System.out.println("C" + "(" + 8 + "," + i + ")" + " ———— "
30 + Binomial(8, i));
31 }
32 }

再看动态规划:

上面紫色字体标出的就是一个动态规划算法的几个关键点:

1)怎么描述问题,要把问题描述为交叠的子问题;

2)交叠子问题的初始条件(边界条件);

3)动态规划在形式上往往表现为填矩阵的形式(在后面会看到,有的可以优化空间复杂度,用一个数组即可,优化也是根据递推式的依赖形式的);

4)填矩阵的方式(或者说顺序)表明了什么?--它表明了这个动态规划从小到大产生的过程,专业点的说就是递推式的依赖形式决定了填矩阵的顺序。

深入讨论:该算法的空间效率如何?能改进吗?

1)空间效率也是nk,参见代码,或者从矩阵上也可看出。

2)可以。为什么可以优化?上面说过,可不可以优化,以及如何优化空间复杂度依赖于它的递推形式:

从填矩阵的那张图可以看出,这个动态规划产生各项的过程(如果按行填的话)是上一行的第 i-1 项和第 i 项加起来产生下一行的第 i 项,传统上,我们从左往右填。

事实上,根据它的产生过程(这个产生过程依赖于递推式自身的数学特征),可以从右往左填,这样用一个数组就行,在原数组上本地不动的填数,从右往左填可以保证一个位置在覆盖以后不会再被用到(这是由递推式的属性决定的,需要画一画才看的比较清楚),这样用一个K大的数组就行了。

由以上实例,相信对于动态规划到底是什么,核心思想,具体的操作细节,以及对于动态规划的理解都加深了吧。

有2点我觉得非常重要:一是填矩阵的顺序,二是动态规划空间复杂度的优化。这2点都跟递推式的依赖关系有关(这是本质),在形式上就表现为填矩阵的时候你的顺序要确保每填一个新位置时你所用到的那些位置(即它依赖的)要已经填好了,在空间优化上表现为当一个位置在以后还有用的时候你不能覆盖它。

三. 装配线调度问题

汽车工厂,两条装配线,每条装备线上有n个装配站,编号为j=1,2,…,n。e[i]表示进入每条装配线所需的时间花费。a[i][j]表示在第i条装配线上第n个装配站装配所需花费的时间。同一装配线上汽车底盘由一个装配站移动到下一个装配站无时间花费。t[i][j]表示从第i条装配线第j个装配站移动到另一装配线j+1号装配线所需的时间代价。

  求最短装配时间以及装配路线。
步骤1:找出最优解的结构 

考虑底盘到装配站S1,j的最快可能路线。若j=1,则底盘能走的只有一条路线。 若j=2,3,……n, 则有两种可能选择,这个底盘可能直接来自S1,j-1,移动代价位0;也可能来自S2,j-1,移动代价为t[2][j-1]。

假设到达S1,j的最快路线通过了装配站S1,j-1;则这个底盘必定是利用了最快的路线从开始点到装配站S1,j-1的。

步骤2:递归定义最优解

利用子问题的最优解来递归定义一个最优解的值。

我们的最终目标是确定底盘通过工厂的所有路线的最快时间,记为f*。

则f*=min{f[1][n]+x1,f[2][n]+x2}

由上边的两个递归式我们可以求得任意f[i][j]的值,以及f*的值。 

步骤3:使用自底向上的方式计算最优值

我们注意到,对于j>=2, f[i][j]的值仅仅依赖f[1][j-1]和f[2][j-1]两个值。要计算f[i][j],必须先计算f[1][j-1]和f[2][j-1]。我们通过以递增装配站编号j的顺序来计算f[i][j]的值。

伪码:

 FASTEST-WAY(a[][],t[][],e[],x[],n)

    f[1][1]=e[1]+a[1][1]; f[2][1]=e[2]+a[2][1]

    for(j=2;j<=n;j++)

    {

           if(f[1][j-1]+a[1][j]<=f[2][j-1]+t[2][j-1]+a[1][j])

                 f[1][j]=f[1][j-1]+a[1][j];

           else f[1][j]=f[2][j-1]+t[2][j-1]+a[1][j];

           if( f[2][j-1]+a[2][j]<=f[1][j-1]+t[1][j-1]+a[2][j])

               f[2][j]=f[2][j-1]+a[2][j];

           else f[2][j]=f[1][j-1]+t[1][j-1]+a[2][j];

    }

    if(f[1][n]+x1<=f[2][n]+x2)

           f*=f[1][n]+x1;

      else f*=f[2][n]+x2;

    return f*;


posted @ 2011-10-07 22:14  程见航  阅读(350)  评论(0)    收藏  举报