動態規劃演算法舉例解析(最大收益和最小損失選擇)
在說動態規劃的例子之前,先說明一下動態規劃和分治演算法的區別
雖然兩者都是通過組合子問題的解來求解原問題但是分治方法將問題劃分為互不相交的子問題,遞迴的求解子問題再將它們的解組合起來求出原問題的解。
而動態規劃演算法應用於子問題重疊的情況,即不同的子問題具有公共的子子問題,在這種情況下,分治演算法會做許多不必要的工作,它會重複的求解這些子問題,儘管這些子問題都曾經計算過。而動態規劃演算法就聰明瞭很多,它對每個子子問題都只求解一次。將其解儲存在一個數據結構中,從而遇到曾經計算過的子子問題並不是再計算而是從這個資料結構直接取結果即可。
學習動態規劃演算法,首先要了解最優子結構這個概念
如果一個問題的最優解包含其子問題的最優解,我們就稱此問題具有最優子結構性質,一個問題如果可以應用動態規劃演算法,那麼它必然具有最優子結構。在使用動態規劃方法時,我們要利用子問題的最優解來構造原問題的最優解。
下面舉一個例子,其實也是書上的例子
例子1,如何切割鋼條,長度為n的鋼條,可以選擇切割成多段也可以選擇不切割。這完全要依靠收益來看。下面這個表示長度為i的鋼條所對應的價格表
長度i 1 2 3 4 5 6 7 8 9 10
價格pi 1 5 8 9 10 17 17 20 24 30
比如一個長度為4的鋼條,它的切割情況就有
方案 收益
(1,3)---------- 9
(2,2)-----------10
(3,1)-----------9
(1,1,2)---------7
(1,2,1)---------7
(2,1,1)---------7
(1,1,1,1)-------4
(4,0)-----------9
這些方案中將長度為4的鋼條切割成(2,2)的收益最大,於是企業就會切割了賣。。。。
首先看這個問題滿足使用DP的最優子結構的性質。
我們第一次選擇切割鋼條的問題,剩下的鋼條則是與原問題相似的子問題。
其實原問題的最優解就是由第一次切割得到的兩段鋼條的最優切割方案組成的,符合最優子結構。這裡為了儘可能減少子問題空間的大小,切割後剩下的鋼條構成一個子問題。剩下的鋼條可能還會被切割,也可能不被切割,這裡我們不管,我們只要求得在當前切割方案下這個子問題的最優解,與當前切割方案組合最優則能得到原問題的最優切割方案。
對於第一步的切割,你假定已經知道第一部應該如何切割得到最優解了,並且你知道第一步切割會產生怎麼的子問題。
可以利用剪下貼上的思想來驗證鋼條切割問題用動態規劃的思想來進行,假設子問題的解不是它自身的最優解,那麼我們可以從原問題中把子問題的解減掉,將子問題一個更優的解放到原問題的最優解中,那麼將形成一個比原問題最優解更優的方法,這與全問題最優解是矛盾的,所以如果要想達到原問題的最優解,必須子問題步步是最優的才是可行的。
將一個長度為4的鋼條切割,僅僅使用了一個子問題,(長度為4-i鋼條的切割),但i的值可能取0,1,2,3,就是第一次要切割的位置在長度為4的鋼條上的位置,下面則是一棵遞迴呼叫樹。可以看出要求長度4的鋼條最優切割方案,我們需要求解長度為3、2的切割方案,對應的則是i=1,i=2,這是剩餘各條需要切割的,節點為1和0的部分不需要切割,所以可以看到它的子樹沒有或者是0.
畫框的部分代表如果不採取措施就會重複計算的部分。樸素的自頂向下的動態規劃會造成重複計算的問題,時間複雜度高,加入了備忘機制的自頂向下的動態規劃可以避免這一問題,將計算出的結果放在列表中,如果還需要這部分值則直接取即可。另外自底向上的動態規劃可以更好的達到備忘機制的效果,想得到4的切割方案,先計算出3的方案,想得到2的切割方案,先得到1的方案,直到0,而0是初始設定,這裡長度為0的鋼條自然是沒有收益的。用了就是先把小粒度的計算出來在得到大粒度的思想。下面針對這三種方案分別實現
package DP;
public class Cut_Rod {
//p為存放了不同長度對應的價格,而n則是要判斷長度n的鋼條最好的切割
private static int[] array;
private static int[] srray;
public static int cut_Rod(int[]p,int n){
//自頂向下樸素的動態規劃
if(n==0){
return 0;
}
int q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++){
q=max(q,p[i]+cut_Rod(p,n-i));
}
return q;
}
public static int memoized_cut_road(int[]p,int n){
//加入了備忘錄的自頂向下的動態規劃
int[] array = new int[n+1];
for(int i=0;i<array.length;i++){
array[i] = Integer.MIN_VALUE;
}
return memorized_cut_road_autx(p,n,array);
}
public static int memorized_cut_road_autx(int[]p,int n,int[]array){
if(array[n]>0){
//說明已經更新完,可以直接取值
return array[n];
}
int q;
if(n==0){
q = 0;
}else{
q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++){
q = max(q,p[i]+memorized_cut_road_autx(p,n-i,array));
}
}
return q;
}
public static int bottom_up_road(int[]p,int n){
//自底向上的動態規劃演算法,先求解出小問題,逐步向上求解
int[] array = new int[n+1];
array[0] = 0;
int q = Integer.MIN_VALUE;
for(int j=1;j<=n;j++){
q = Integer.MIN_VALUE;
for(int i=1;i<=j;i++){
q = max(q,p[i]+array[j-i]);
}
array[j] = q;
}
return array[n];
}
public static void extended_bottom_up_rod(int[]p,int n){
//不僅求出切割的最大效益,並且儲存切割方案,array儲存的是子問題的解避免重複計算,而sarry儲存的則是每步的最優切割方案
array = new int[n+1];
srray = new int[n+1];
array[0]=0;
int q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++){
q = Integer.MIN_VALUE;
for(int j=1;j<=i;j++){
if(q<p[j]+array[i-j]){
q = p[j]+array[i-j];
srray[i] = j;
}
}
array[i] = q;
}
}
public static void print_road_solution(int[]p,int n){
extended_bottom_up_rod(p,n);
/*for(int i=1;i<=n;i++){
System.out.println(srray[i]);
}
System.out.print("-------");*/
while(n>0){
System.out.print(srray[n]);
n = n-srray[n];
}
}
static int max(int n1,int n2){
return n1>n2?n1:n2;
}
public static void main(String[] args) {
int[] p={0,1,5,8,9,10,17,17,20,24,30};
/*System.out.print(cut_Rod(p,4));*/
/* System.out.print(memoized_cut_road(p,4));
System.out.print(bottom_up_road(p,4));*/
print_road_solution(p,9);
}
}
例子2,矩陣鏈乘法,給定一個n個矩陣的序列<A1,A2.......An>,我們計算矩陣鏈乘積A1*A2*....An,用加括號的方式來明確計算次序,然後利用標準的矩陣相乘演算法進行計算,使得計算量最小,這裡的計算量用標量乘法的次數來表示作為計算的代價。
比如A1為10*100,A2為100*5,,A3為5*50,(A1*A2)*A3的計算量為10*100*5+10*5*50=7500,A1*(A2*A3)的計算量為10*100*50+100*5*50=75000,顯然按照第一種計算順序的計算代價更小一些,這裡就是在這樣的背景下提出了利用三種方式(自頂向下、加入備忘機制、自下向上)來分別計算的。
package DP;
public class Matrix_Multiply {
public static int[][]m;
public static int[][]s;
public static int recursive_Matrix_Chain(int[]p,int i,int j){
if(i==j){
return 0;
}
int q = Integer.MIN_VALUE;
int[][] m = new int[j+1][j+1];
m[i][j] = Integer.MAX_VALUE;
for(int k=i;k<=j-1;k++){
q = recursive_Matrix_Chain(p,i,k)+recursive_Matrix_Chain(p,k+1,j)+p[i-1]*p[k]*p[j];
if(q<m[i][j])
m[i][j] = q;
}
return m[i][j];
}
//加入了備忘機制d
public static int memoized_martix_chain(int[] p){
int n = p.length-1;
int q = Integer.MIN_VALUE;
int[][] m = new int[n+1][n+1];
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
//可見是矩陣的上三角部分
m[i][j] = Integer.MAX_VALUE;
}
}
return lookup_Chain(m,p,1,n);
}
public static int lookup_Chain(int[][]m,int[]p,int i,int j){
int q = Integer.MIN_VALUE;
if(m[i][j]<Integer.MAX_VALUE){
//只說明一種情況就是所求的值之前已經計算過,不需要重複計算,直接取值即可
return m[i][j];
}
if(i==j){
m[i][j] = 0;
}
else{
for(int k=i;k<=j-1;k++){
q = lookup_Chain(m,p,i,k)+lookup_Chain(m,p,k+1,j)+p[i-1]*p[k]*p[j];
if(q<m[i][j]){
m[i][j] = q;
}
}
}
return m[i][j];
}
public static void matrix_chain_Order(int[] p){
int n = p.length-1;
m = new int[n+1][n+1];
s = new int[n+1][n+1];
for(int i=1;i<=n;i++){
m[i][i] = 0;
}
for(int l=2;l<=n;l++){
for(int i=1;i<=n-l+1;i++){
int j = i+l-1;
m[i][j] = Integer.MAX_VALUE;
for(int k=i;k<=j-1;k++){
int q = m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(q<m[i][j]){
m[i][j] = q;
s[i][j] = k;
}
}
}
}
}
public static void print_matrix(int[] p,int i,int j){
matrix_chain_Order(p);
print_optimal_parens(i,j);
}
public static void print_optimal_parens(int i,int j){
if(i==j){
System.out.print("A"+i);
}else{
System.out.print("(");
print_optimal_parens(i,s[i][j]);
print_optimal_parens(s[i][j]+1,j);
System.out.print(")");
}
}
public static void main(String[] args) {
int[] p ={30,35,15,5,10,20,25};
/*System.out.print(recursive_Matrix_Chain(p,1,6));*/
/*print_matrix(p,1,6);*/
System.out.print(memoized_martix_chain(p));
}
}