1 1,1
3 2 2,1 2,2
4 10 1 3,1 3,2 3,3
4 3 2 20 4,1 4,2 4,3 4,4
(a)數字三角形 (b)狀態編號
從第一行的數開始,每一次可以向下或者向右走一格,一直走到最下行,把沿途的數全部加起來,如何使這個和最大?
如果熟悉回溯法,會發現這是一個動態決策問題,每次有兩種選擇,左下或者右下.用回溯法求出所有可能的路線,就可以從中選擇最優路線,但是有路線2的n次方條,效率不可忍受
可以用抽象的方法來思考問題,把當前位置看成一個狀態,用(i,j)位置來表示.然後定義狀態(i,j)的指標函數,d(i,j)為從格子(i,j)出發出發能獲得的最大值
看不同的狀態如何轉移,往左走,則走到(i+1,j),往右走,則走(i+1,j+1),自由選擇,選擇其中較大的一個
狀態轉移方程 d(i,j)=a(i,j)+max{d(i+1,j),d(i+1,j+1)}
如果從(i+1,j)出發得到部分和是最大的,則加上a(i,j)之後也是最大的 .這個性質叫做最優子結構
動態規劃的核心是狀態和狀態轉移方程.
實現計算
1.遞迴計算
int d(int i,int j){ return a[i][j]+(i==n? 0:d(i+1,j)>d(i+1,j+1));}
效率太低,遞迴時調用關係樹造成了重複計算
2.遞推計算
int i,j;for(j=1;j<=n;j++) d[n][j]=a[n][j];for(i=n-1;i>=1;i—) for(j=1;j<=i;j++) d[i][j]=a[i][j]+d[i+1][j]>?d[i+1][j+1];
遞推的時間複雜度為 狀態總數x每個狀態的決策個數x決策時間
3.記憶化搜尋
int d(int i,int j){ if(d[i][j]>=0) return d[i][j]; return d[i][j]=a[i][j]+(i==n?0:d(i+1,j)>?d(i+1,j+1));}
把計算結果儲存在d[i][j]中,每次記錄是否計算過
也可以用另一個數組vis[]儲存是否被計算過
0-1背包問題
n種物品,每種有無窮多個,第i種體積為vi,重量是wi,選一些到容量為c的背包中,使得背包內物體在總體積不超過c的前提下重量盡量大
剛才的方法已經不適用了,原來的狀態轉移太混亂了.要消除這種混亂,要讓決策有序化.這就是多段決策問題
每次做一次決策就可以得到解的一部分.所有的決策做完就得到最終解.回溯法中每個決策對應一顆子樹,每個解答書對應一個解.節點層數是下一個待填充位置cur,就是即將完成的決策序號,在動規中稱為階段
多段決策可用動態規劃解決.d(i,j)表示第i層,背包剩餘容量為j的最大重量和.
d(i,j)=max{d(i+1,j),d(i+1,j-V[i])+w[i]}
邊界是i>n時d(i,j)=0,j<0時為負無窮
白話一點,d(i,j)表示把第i,i+1,i+2,,,n個物品裝到容量為j的背包中的最大重量和,事實上,這個說法常用階段和層這樣的術語,
代碼(結果是d[1][c])
for(int i=n;i>=1;i—) for(int j=0;j<=c;j++) { d[i][j]=(i==n? 0:d[i+1][j]); if(j>=v[i]) d[i][j]>?=d[i+1][j-v[i]]+w[i]; }
i必須逆序枚舉,但j的迴圈次序是無關緊要的
還有一種對稱的狀態定義,用f(i,j)表示把前i個物品裝到容量為j的背包中的最大重量和
狀態轉移方程: f(i,j)=max{f(i-1,j),f(i-1,j-v[i])+w[i]}
最終答案為g(n,C)
for(int i=1;i<=n;i++) for(int j=0;j<=C;j++) { f[i][j]=(i==1? 0:f[i-1][j]); if(j>=v[i]) f[i][j]>?=f[i-1][j-v[i]]+w[i]; }
動態規劃的類型很多,今天就介紹最優子結構和多段決策,還有樹形動規,集合動規,DAG動規日後為一一研究介紹