1. 程式人生 > >算法導論——動態規劃

算法導論——動態規劃

lse ++ 結合 com 效率 不包含 tab 給定 lib

  動態規劃指的是一個問題可以拆分成多個小的最優子問題,並且這些子問題具有重疊,典型的如斐波那契數列:f(2)=f(1)+f(0),f(3)=f(2)+f(1),f(4)=f(3)+f(2),若使用簡單的遞歸算法求f(4),則f(2)會被計算兩次,當計算f(n)時需要計算f(n-1)和f(n-2)而f(n-1)又要計算記一次f(n-2),如此往復時間復雜度為n的指數級別,導致算法效率低下。若能夠記錄f(2)至f(n-1)的結果,可以保證每一級計算都是O(1)復雜度,整個算法的時間復雜度就能下降至O(n),空間復雜度O(n)。必須保證拆分後的子問題是當前規模下的最優解,才可保證遞歸邏輯的正確,典型的例子是無權最短路徑問題,若已知A到除最終目的地B外的所有點最短路徑,則只需遍歷尋找與B直接相鄰所有點到A最近的一個。

通常動態規劃可以分為4個步驟:

  1. 刻畫一個最優解的結構特征
  2. 遞歸地定義最優解的值
  3. 計算最優解的值,通常采用自底向上的方法
  4. 利用計算出的信息構造一個最優解

因此,動態規劃的關鍵是分析子問題的拆分與遞歸式。下面四個問題來自《算法導論》第三版。

鋼條切割

  有一條長度為n的鋼條,可以不計成本的切割成多條鋼條出售,不同長度與價格關系如下表所示,求如何切割獲得最大的利益rn

長度i

1

2

3

4

5

6

7

8

9

10

價格pi

1

5

8

9

10

17

17

20

24

30

以長度n=4為例,分割共有以下幾種方案

n=4, r=9

n=1+3, r=9

n=1+1+2, r=7

n=1+1+1+1, r=4

n=2+2, r=10

最佳方案為分成2+2兩端,利潤為10

  對於長度為n的鋼條,其可以通過切割獲得的最大利益記為rnrn=max(pn,r1+rn-1,r2+rn-2,...rn-1+r1) rn的最大利潤可能有兩種情況:不切割或者先切為兩段,該兩段各自的ri+rn-i為最大值。因此可以采用遞歸的方式,求出rn的值,偽代碼如下:

1 int cutRod(p,n){
2     if(n==0)
3         return 0;
4     q=-1
5     for(i=1;i<=n;i++){
6 q=max(q,p[i]+cutRod(p,n-i)); 7 } 8 return q; 9 }

  該算法的問題是效率太低,原因在於cutRod(p,i)這個值在不同階段被分別計算了多次,比如要求長度為2的鋼條的最大利益,要計算分割成1+1的利益,這裏r1被計算了兩次。如果能夠記錄r1rn-1的值,可以大幅度提交計算效率,這是一種典型的空間換取時間的方法——動態規劃算法。

動態規劃有兩種等價的實現方法:

  第一種,帶備忘的自頂向下法。在之間遞歸算法調用每一層的時候,先檢查該值有沒有被計算過,若沒有,調用並存儲;若計算過,直接取出該值。偽代碼如下:

 1 int memoizedCutRod(p,n){
 2     r[n+1] //用於記錄r0到rn-1的值
 3     for(i=0;i<=n;i++){
 4         r[i]=-1;
 5     }
 6     return memoizedCutRodAux(p,n,r);
 7 }
 8 
 9 int memoizedCutRodAux(p,n,r){
10     if(r[n]>=0)
11         return r[n];
12     if(n==0)
13         q=0;
14     else
15     {
16         q=-1;
17         for(i=0;i<=n;i++){
18             q=max(q,p[i]+ memoizedCutRodAux(p,n-i,r));
19         }
20     }
21     r[n]=q;
22     return q;
23 }

  第二種,自底向上法,將一個問題分成規模更小的子問題,從小到大進行求解,當求解至原問題時,所需的值都已求解完畢。對於分割鐵棒問題來說,從長度為1一直求解至長度為n時最佳分割方案的收益。由於rn=max(pn,r1+rn-1,r2+rn-2,...rn-1+r1),只需計算出r1rn-1的值,便可計算出rn的值。偽代碼如下:

 1 int bottomUpCutRod(p,n){
 2     r[n+1] //用於記錄r0到rn-1的值
 3     s[n+1]//若要輸出分割長度,則需要記錄不同長度最大利潤的分割情況
 4     r[0]=0;
 5     for(j=1;j<=n;j++){
 6         q=-1;
 7         for(i=1;i<=j;i++){
 8             //針對長度為j時,遍歷所有的分割情況,尋找到最佳的結果
 9             if(p[i]+r[j-i]>q){
10                 q= p[i]+r[j-i];
11                 s[j]=i;//記錄分割位置
12             }
13         }
14         r[j]=q;
15     }
16     return r[n];
17 }
18 void printCutRod(s,n){
19     if(s[n]!=0)
20         printCutRod(s,s[n]);
21     printf("%d ",s[n]);//此處輸出的為所有的分割位置
22 }

矩陣鏈乘法

  矩陣相乘是符合結合律的, A1A2A3= A1(A2A3),但是兩者的計算規模可能是不同的。假設三個矩陣的大小分別是 10*100、100*5、 5*50,則 A1A2A3的計算次數為 10*100*5+10*5*50=7500,而 A1(A2A3)的計算規模為 100*5*50+10*100*50=75000,兩者相差了 10 倍的規模。對於一組給定的矩陣相乘A1A2A3 ? An要求出如何進行乘法結合可以進行最少的計算次數。

  下面使用形如A1?n來表示A1A2A3 ? An的最終乘積結果。對於A1?n的最少計算式,其必定在Ak 處進行了分割 (A1A2A3 ? Ak)(Ak+1Ak+2 ? An),總計算次數為 m,i, j- = min{m,i, k- +m,k + 1, j- + pi?1pkpj}, i ≤ k < j,故必須要先求出A1A2A3 ? Ak和Ak+1Ak+2 ? An各自的最少計算次數然後遍歷計算出最小值。 因此可以采用自 1 int matrixChainOrder(p){

 2     n=p.length-1;
 3     m[n][n],s[n][n];//m記錄矩陣鏈各自的最少計算次數,s記錄最少時分割位置
 4     for(i=0;i<=n;i++)
 5         m[i][i]=0;
 6     for(l=2;l<=n;l++){//l限制矩陣鏈的長度,先計算出所有2個矩陣相乘的最少次數,然後是3個矩陣,直至n個矩陣
 7         for(i=1;i<=n-l+1;i++){
 8             j=i+l-1;
 9             m[i][j]=INFI;
10             // m[i,j]=min{m[i,k]+m[k+1,j]+p_(i-1) p_k p_j },i≤k<j
11             for(k=i;k<=j-1;k++){
12                 q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
13                 if(q<m[i][j]){
14                     m[i][j]=q;
15                     s[i][j]=k;
16                 }
17             }
18         }
19     }
20     //此處可添加打印分割結果的代碼
21     return m[n];
22對於X = x1x2 ? xm和Y = y1y2 ? yn的 LCS 記為Z = z1z2 ? zk最優子結構拆分:

1. 如果xm = yn,則zk = xm = yn且Zk?1是Xm?1和Yn?1的一個 LCS
2. 如果xm ≠ yn且zk ≠ xm,則 Z 是Xm?1和 Y 的一個 LCS
3. 如果xm ≠ yn且zk ≠ yn,則 Z 是 X 和Yn?1的一個 LCS
用 c[i, j] 表 示XiYj 的 LCS 長 度 , 可 列 出 遞 歸 式

技術分享圖片

 1 int lcsLength(X,Y){
 2     m=X.length;
 3     n=Y.length;
 4     b[m][n];//記錄最優解的構造
 5     c[m+1][n+1];
 6     for(i=1;i<=m;i++)
 7         c[i][0]-0;
 8     for(j=0;j<=n;j++)
 9         c[0][j]=0;
10     for(i=1;i<=m;i++){
11         for(j=1;j<=n;j++){
12             if(x[i]==y[i]){
13                 c[i][j]=c[i-1][j-1]+1;
14                 b[i][j]="";
15             }
16             else if(c[i-1][j]>=c[i][j-1]){
17                 c[i][j]=c[i-1][j];
18                 b[i][j]="";
19             }
20             else{
21                 c[i][j]=c[i][j-1];
22                 b[i][j]="";
23             }
24         }
25     }
26     printLcs(b,X,m,n);
27     return c[n];
28 }
29 
30 void printLcs(b,X,i,j){
31     if(i==0||j==0)
32         return;
33     if(b[i][j]==""){
34         printLcs(b,X,i-1,j-1);
35         print x[i];
36         return;
37     }
38     if(b[i][j]==""){
39         printLcs(b,X,i-1,j);
40         return;
41     }
42     printLcs(b,X,i,j-1);
43 }

最優二叉搜索樹

對於搜索樹來說,不同節點的搜索頻率是不同的,節點離根越遠搜索時間就越長,所以我們希望將搜索頻率高的節點放在離根近的位置,使得整體的效率期望值最優。但是,並不是簡單地把搜索頻率最高的點做根節點就行了,其余節點的深度增加反而可能導致整體效率降低,極端情況最小值搜索頻率最高,若作為根節點,整棵樹的平衡性很差,反而容易導致搜索效率的降低。

對於一個二叉搜索樹,有n個關鍵字k1,k2,...,kn和n+1個偽關鍵字d0,d1,d2,...dn,其中d0代表小於k1的搜索結果,d1是大於k1小於k2的搜索結果,搜索k是成功的搜索,而搜索d是失敗的搜索,所以d一定是葉子節點且di和di-1一定是ki的兩個子節點。對這樣節點的最優二叉搜索樹來說,他含有根節點和兩棵子樹,包含連續的關鍵字ki,ki+1,...,kj和對應的偽關鍵字,該子樹必定是對應規模的最優二叉搜索樹,否則只需將該規模下的最優二叉搜索樹替換該子樹就會產生搜索期望值更小的樹,這與最優二叉樹的假設矛盾。對於特殊情況j=i-1時,樹不包含實際關鍵字,僅含有偽關鍵字di-1。p為關鍵字的搜過概率,q為偽關鍵字的搜索概率,對於一般情況,需要從ki,ki+1,...,kj中選擇根節點kr來構造最優二叉搜索樹。當該樹成為目標結果的子樹時,因在子樹的期望值基礎上增加所有點的概率之和技術分享圖片,因為每個點的深度都增加了1。對於給點的節點條件,只需尋找到使左右子樹的期望值加上所有節點概率之和最小即為最優二叉搜索樹(左右子樹所有節點加上根節點的權是1),因此可以對期望搜索代價列出遞歸式技術分享圖片

 1 double optimal-bst(p,q,n){
 2     e[n+2][n+1];//e[1..n+1][0..n]記錄期望值
 3     w[n+2][n+1];//w[1..n][0..n]記錄i到j的概率和避免重復計算
 4     root[n+1][n+1];//root[1..n][1..n]記錄所有樹的根節點
 5     for(i=1;i<=n+1;i++){
 6         //初始化j=i-1的特殊情況
 7         e[i][i-1]=q[i-1];
 8         w[i][i-1]=q[i-1];
 9     }
10     for(l=1;l<=n;l++){//l表示關鍵字的個數,先計算1個實際關鍵字的樹,然後2個依次增加
11         for(i=1;i<=n-l+1;i++){
12             j=i+l-1;
13             e[i][j]=INFI;
14             w[i][j]=w[i][j-1]+p[i]+q[j];//計算w[i][j]
15             for(r=i;r<=j;r++){
16                 t=e[i][r-1]+e[r+1][j]+w[i][j];//計算e[i,r-1]+e[r+1,j]+w(i,j)
17                 if(t<e[i][j]){
18                     e[i][j]=t;
19                     root[i][j]=r;
20                 }
21             }
22         }
23     }
24     //省略了打印代碼
25     return e[1][n];
26 }

算法導論——動態規劃