動態規劃

對於動態規劃,我以前的感覺就是在算法的運行中記錄下一些計算的結果。這就是我所有對動態算法的理解了。當然,這是動態算法的一個特點。但是,要對一個問題運用動態規劃時就分析不出來。也看不出動態規劃算法的核心。
其實,動態算法一個最大的特點就是最優子結構:就是原問題的解包含了子問題的解。
首先來看看動態規劃需要的原理:
要運用一個原理肯定要知道原理所適合的場合。竟然是有場合要求的,那麼我們就先要找到這個場合的特點。
動態規劃其適用的場合爲:具有最優子結構和重疊子問題。
①最優子結構:原問題的最優解包含了子問題的最優解。(這裏當然還不是那麼清楚)
②重疊子問題:就是在求解最優解時我們解的子問題的最優解會重複出現。
要明白這兩個問題我們需要看幾個例子:
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)。




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