动态规划

对于动态规划,我以前的感觉就是在算法的运行中记录下一些计算的结果。这就是我所有对动态算法的理解了。当然,这是动态算法的一个特点。但是,要对一个问题运用动态规划时就分析不出来。也看不出动态规划算法的核心。
其实,动态算法一个最大的特点就是最优子结构:就是原问题的解包含了子问题的解。
首先来看看动态规划需要的原理:
要运用一个原理肯定要知道原理所适合的场合。竟然是有场合要求的,那么我们就先要找到这个场合的特点。
动态规划其适用的场合为:具有最优子结构和重叠子问题。
①最优子结构:原问题的最优解包含了子问题的最优解。(这里当然还不是那么清楚)
②重叠子问题:就是在求解最优解时我们解的子问题的最优解会重复出现。
要明白这两个问题我们需要看几个例子:
a.装配线调度
问题描述:
每一个装配线S上有多个装配站a,每个装配站的时间也不同,且能够将装配物在两装配线的站点之间移动,但移动需要耗时t。那么在当一件物品进入站配站时,走哪条路会是一个最短耗时。
当然,这看一下不就是一个最短路径的求取吗?呵呵,是的。当然可以用最短路径了,但是你可以对比一下最短路径算法与下面算法的时间效率。
开始分析问题:(这里分析问题采用动态规划四部曲:1.确定最优解结构。2.找出最优解的迭代关系。3.按自底向上的方式计算最优解的值。4由计算的结果构造一个最优解)
1.最优解结构
 首先可以具体化一下。我们可以先求a(1,1):f(a(1,1)) = e1.在有f(a(2,1))=e2。然后,我们计算a(1,2),这个有两条可能的路径:a(1,1)→a(1,2)和a(2,1)→a(1,2),这两种的计算方式如下:f指的为路劲长度
f(a(1,1)→a(1,2)) = f(a(1,1)) + a(1,2)
f(a(2,1)→a(1,2)) = f(a(2,1)) + t(2,1) + a(1,2)
然后我们取两者中的小者。
开始一般化:
(a).对于装配线1中的第i个装配站有
      f(a(1,i) = f(a(1,i-1)) + a(1,i)
      or
      f(a(1,i)) = f(a(2,i-1)) + t(2,i-1)+a(1,i)
   取两者的小者,则我们可以写出这个函数解的递归式
   f(a(k,i)) = 1    if i = 1 
   f(a(k,i)) = min(f(a(k,i-1)) + a(k,i),f(a(~k,i-1)) + t(~k,i-1)+a(k,i));  //其中k是我们装配线的号:k=1,2. ~k为取反。即~1=2,~2=1.
这个是解的结构,那么用递归来解不是很直白吗?但是,我们要考虑的是:这样求解的效率怎么样呢?
当我们来这样分解吧:
取i=4,求f(1,4)
f(a(1,4)) = f( a(1,3)) + a(1,4)  或者 f(a(1,4)) = f(a(2,3) + t(2,3) + a(1,4)
f(a(1,3)) = f(a(1,2)) + a(1,3) 或者 f(a(1,3)) = f(a(2,2)) + t(2,2) + a(1,3)  。。。。。。。。。。。。(1)
f(a(2,3)) = f(a(2,2)) + a(2,2) 或者 f(a(2,3)) = f(a(1,2)) + t(1,2) + a(2,2)  。。。。。。。。。。。。(2)
注意了,在(1)中我们要计算f(a(1,2)) 和f(a(2,2)),且在(2)中我们也要计算f(a(2,2))和f(a(1,2)).即,这两个解是重复的计算了(子问题重叠),那么只要我们在计算的时候能够保存下来不就避免了重复计算了吗?恩,是的。这就是动态规划对付重叠子问题的方法。
重叠子问题是搜喽出来了,那么最优子结构又是什么呢?
首先,我们可以这样看待求解f(a(1,4)).我们像神一般,直接的知道f(a(1,4))是根据f(a(1,4)) = f( a(1,3)) + a(1,4)这个表达式计算得来的。那么,这个问题中肯定包含了f(a(1,3))的最优问题。这证明是很简单的,如果f(a(1,4))中的f( a(1,3))不是最优的,那么f( a(1,4))也不会是最优的,因为,我们完全可以用f( a(1,3))的最优路径来代替上面的f( a(1,3))路径,从而得到最优路径,要是这样,f( a(1,4))就不是最优的了。这就是说神的判断错误了。
恩,这样动态规划的两个问题就都解决了。
2.确定解的迭代关系
在上面的分析时就包含了解的递归关系了。
      f(a(k,i) = f(a(k,i-1)) + a(k,i)
      or
      f(a(k,i)) = f(a(~k,i-1)) + t(~k,i-1)+a(k,i)
3.自底向上的设计
     现在是要实现这个算法,那么我们的思路是根据上面的递归关系,但我们要加入一条就是要记录子问题的解。即我们要一个数组来记录一个子问题的解:sub(2,n).sub(1,i)为第1个装配线上的第i个装配站,sub(2,i)为第2个装配线上的第i个装配站.
恩,接近一步了。我么这里不采用自顶向下的算法,而是采用自底向上的算法。即,首先计算最底下的东西,然后再一步步的往上计算。
即首先我们计算sub(1,1),sub(2,1);然后计算sub(1,2),sub(2,2),......,sub(1,i),sub(2,i),......,sub(1,n),sub(2,n)。这样是不是感觉特别的好理解了。呵呵,动态规划思想也不复杂是吧。是的,算法的思想是不复杂了,复杂的是你要分析问题,怎么分析出最优解结构,证明有最优子结构。
4.打印解
    当我们求出一个最优解的时候,那么怎么打印这个解呢?呵呵,我们需要一个数组来记录下来我们走过的路径。road(2,n): road(1,i)记录第1装配线第i个装配站是从哪个装配线来的。
例如,road(1,4) = 2 那么这个说明是从装配线2来的,即从a(2,3)来的。呵呵,下一步就是去找road(2,3)了。这样我们只要一个for循环就能解决了是不。

下面再来求矩阵连乘的问题:
矩阵连乘问题是找到一种结合方式来使矩阵链的乘法次数最少。矩阵链为A1A2...Ai...An
1.最优解结构
  很简单,就是找到一种加括号的方式使得矩阵链能够乘法次数最少。当然,我是看了书的。所以我能知道怎么分析了。试想一下,矩阵链乘法最终是不是变成两个矩阵的相乘呢?不管你有多少矩阵,最终我们要成的就是两矩阵。所以,我们开始将这个链用两对括号来分开,现在我是神,我知道在k处方分开是最优的,即:(A1A2...Ak)(Ak+1...An).那么,对于A1A2...Ak)子问题也是一样的,原问题肯定包含这个子问题的最优解。
2.解的递归特性
 问题的递归特性其实很好写出来的。即
f(A1A2...Ai...An) = f(A1A2...Ak)+f(Ak+1...An) + p1pkpn.//当然,这是对于1到n的解。当我们将1变成i和n变成j时,这就成了一个一般化的问题了。(p1pkpn需要了解矩阵的乘法,p1为A1的行,pk为Ak的列,pn为An的列,最终结果的矩阵大小为pkxpn)。
3.自底向上的解法
首先,我们是需要一个变量来保存每个底层计算的解。sub(i,j)表示Ai到Aj子矩阵链的最优解。对於单个矩阵我们不需要计算,即sub(i,j) = 0 ;
那么我们就现计算两个两个的矩阵最优值,然后计算三个三个的矩阵最优值,...,最后计算所有链长矩阵的最优值。
当然,我们不是神,所以每次的k选取都是要经过循环穷举选取的。
for  L = 2 to n
     for  i = 1 to n-l + 1 //即,i在每次计算子矩阵链时最终到哪个地方停止。
            j = i+L-1   //子矩阵从i到那个地方结束
            sub(i,j) = MAX
           for k = i to j-1 //便利每一个位置,来找到最优值,注意,(Ai,...,Aj)和(Ai,...,Aj-1)(Aj)是一回事情
                    tmp = sub(i,k) + sub(k+1,j)
                    if(tmp < sub(i,j))  
                                 sub(i,j) = tmp ;
                                  road(i,j) = k ;
4.打印解
    现在我们知道怎么求最小解了,但不知道怎么加括号了。嘻嘻,只要我们在求解的时候记录下每个子问题的最优分割处,我们就能知道怎么打印解了。当然,这里需要我们有一个记录变量road(n,n).road(i,j)=k,表示[i,j]子矩阵链在k处划分最优。那么我们加到求解过程中去。然后我们该怎么打印呢?
print(road,i,j)
  if(i >= j-1 ) 
         return ;
   printf  '(A' i 
    print ( road,i,road(i,j))
    printf 'A' road(i,j) ')(A' road(i,j) + 1
    print( road,road(i,j) + 1,j)
    printf 'A' j ')'


三、最长公共子序列
     是不是感觉题目难呢?是的了,这些都是最主要的一步:找到解的结构,寻找出最优子结构。
最长公共子序列讲的是,对于X=<x1,x2,...,xm>和Y=<y1,y2,...,yn>寻找一个最长子序列Z,使得Z即是X的子序列也是Y的子序列。这里子序列的概念是(对于X来讲):子序列的元素是X中的,且在子序列出现的相对顺序与在X中出现的相对顺序是相同的。例如,X=<A,B,B,C,D>则,<A,C,D>,<A,B,B>都是X的子序列。
明晰了概念后,我们来找最长公共子序列的解的结构:
根据定义,解的结构是很明确:即找一个公共子序列,且在所有的子序列中最长之一。这很容易想到一个穷举的算法:对X我们计算出所有的子序列(这用10101010编码能很容易实现的),对Y计算出所有的子序列;然后比较相同长度的X,Y子序列,看是否相等,这样我们可以求出最长公共子序列了。但是这种算法好吗?时间耗费太大了。光求X的子序列就得要耗费o(2^m)。所以,这种方法还是行不通的。那么找其它的方法了。
动态规划(先试一下啥,不行再另外找了)
要用动态规划,首先得找到满足动态规划的两个特性:最优子结构和子问题重叠。
先找最优子结构:原问题的解包含了子问题的解。
最常用的最优子结构找法就是先假设我们找到了原问题的最优解了,然后找问题最优解的递归方法,最后证明最优子结构的存在。
那么,
第一步:假设Z=<z1,z2,...,zk>是我们找到的一个最优解。
第二步:找解的递归形式。那么,该怎么找递归形式呢?这是最难的一步。想想,递归的形式是什么?一般我们做递归是不是写成f(n)的形式,然后在计算f(n)的时候去找f(n-1)?哈哈,是的。这是理论一般化。我们也可以用到这里来!先找zk,那么zk会出现在哪里呢?zk当然可以出现在任何地方。但我们找的时候一般找最后面的,即xm和yn。更近一步了,假设zk=xm,那么当zk=yn时我们只要去在<x1,..,xm-1>,<y1,...,yn-1>中找zk-1了。当然,还有其它可能情况,即zk ≠xm,此时我们肯定知道xm≠yn,则我们可以直接的在<x1,..,xm-1>,<y1,...,yn-1>中找zk了。当然,当zk≠xm时,也可以写成这样:在<x1,..,xm-1>,<y1,...,yn>中找zk!对于,zk≠yn,就和上面一步的一样了。
下面为对解的递归式子:令sub(i,j)为X长度为i,Y长度为j的最终公共子序列
               { sub(i-1,j-1) +xi   if xi = yj                 ①
sub(i,j) =  |
               { max( sub(i-1,j), sub(i,j-1) )  others     ②//这里在实际的时候我们是不知道zk是否等于xi,所以只有计算全部来找。
第三步:递归式解出来了,那么就来证明最优子结构的存在了。当为①时,我们知道sub(i-1,j-1)一定是这个子问题的最优解,所以原问题包含了子问题的解。当选②时,原问题的最优解就是这个子问题的最优解,所以也包含了子问题的最优解。因此,这是具有最优子结构的。
最后是要找重叠子问题,这里只要你多写几步就能看到很多问题是重复的计算了。
根据递归,我们能写出一个很优美但时间运行漫长的算法。
这里要利用动态规划的思想,那么就要用记录的方式来省下计算的时间,且想到用自底向上的算法。
那么在动态过程中我们要记录什么呢?就是,我们先求两个长度为1的;然后求一个长度为1,另一个长度为2的,。。。。。那么过程中我们需要记录这样的最优解式子。我们记为sub(i,j).即递归状态中的记法了。
下面为算法的伪代码描述:
 //initial
  sub(1,1...n) = 0 ;
   sub(1...m,1) = 0 ;
   k = 1 ;
   for i =1:m
        for j=1:n
             if xi == yj
                       sub(i,j) = sub(i-1,j-1) + 1 
                        road(k) = xi 
                        k = k + 1
             else 
                     if sub(i-1,j) ≥sub(i,j-1)
                            sub(i,j) = sub(i-1,j)
                     else
                            sub(i,j) = sub(i,j-1)
如果要求我们打印解呢?这里我们只求出了解的最大长度值,而没有记录解的最优路径。那么,该怎么记录呢?我们可以用一个m长的数组来记录。即road(m)。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章