第十五章:動態規劃--裝配線調度html
前言:動態規劃的概念ios
動態規劃(dynamic programming)是經過組合子問題的解而解決整個問題的。分治算法是指將問題劃分爲一些獨立的子問題,遞歸的求解各個問題,而後合併子問題的解而獲得原問題的解。例如歸併排序,快速排序都是採用分治算法思想。本書在第二章介紹歸併排序時,詳細介紹了分治算法的操做步驟,詳細的內容請參考:http://www.javashuo.com/article/p-vyjgdlgg-hq.html。而動態規劃與此不一樣,適用於子問題不是獨立的狀況,也就是說各個子問題包含有公共的子問題。如在這種狀況下,用分治算法則會重複作沒必要要的工做。採用動態規劃算法對每一個子問題只求解一次,將其結果存放到一張表中,以供後面的子問題參考,從而避免每次遇到各個子問題時從新計算答案。算法
動態規劃與分治法之間的區別:
(1)分治法是指將問題分紅一些獨立的子問題,遞歸的求解各子問題
(2)動態規劃適用於這些子問題不是獨立的狀況,也就是各子問題包含公共子問題編程
動態規劃一般用於最優化問題(此類問題通常有不少可行解,咱們但願從這些解中找出一個具備最優(最大或最小)值的解)。動態規劃算法的設計分爲如下四個步驟:數組
(1)描述最優解的結構數據結構
(2)遞歸定義最優解的值學習
(3)按自低向上的方式計算最優解的值測試
(4)由計算出的結果構造一個最優解優化
動態規劃最重要的就是要找出最優解的子結構。書中接下來列舉4個問題,講解如何利用動態規劃方法來解決。動態規劃的內容比較多,我計劃每一個問題都認真分析,寫成日誌。今天先來看第一個問題:裝配線調度問題spa
二、問題描述
一個汽車公司在有2條裝配線的工廠內生產汽車,每條裝配線有n個裝配站,不一樣裝配線上對應的裝配站執行的功能相同,可是每一個站執行的時間是不一樣的。在裝配汽車時,爲了提升速度,能夠在這兩天裝配線上的裝配站中作出選擇,便可以將部分完成的汽車在任何裝配站上從一條裝配線移到另外一條裝配線上。裝配過程以下圖所示:
裝配過程的時間包括:進入裝配線時間e、每裝配線上各個裝配站執行時間a、從一條裝配線移到另一條裝配線的時間t、離開最後一個裝配站時間x。舉個例子來講明,如今有2條裝配線,每條裝配線上有6個裝配站,各個時間以下圖所示:
從圖中能夠看出按照紅色箭頭方向進行裝配汽車最快,時間爲38。分別如今裝配線1上的裝配站一、3和6,裝配線2上裝配站二、4和5。
三、動態規劃解決步驟
(1)描述經過工廠最快線路的結構
對於裝配線調度問題,一個問題的(找出經過裝配站Si,j的 最快線路)最優解包含了子問題(找出經過S1,j-1或S2,j-1的最快線路)的一個最優解,這就是最優子結構。觀察一條經過裝配站S1,j的最快線路,會發現它一定是通過裝配線1或2上裝配站j-1。所以經過裝配站的最快線路只能如下兩者之一:
a)經過裝配線S1,j-1的最快線路,而後直接經過裝配站Si,j;
b)經過裝配站S2,j-1的最快線路,從裝配線2移動到裝配線1,而後經過裝配線S1,j。
爲了解決這個問題,即尋找經過一條裝配線上的裝配站j的最快線路,須要解決其子問題,即尋找經過兩條裝配線上的裝配站j-1的最快線路。
(2)一個遞歸的解
最終目標是肯定底盤經過工廠的全部路線的最快時間,設爲f*,令fi[j]表示一個底盤從起點到裝配站Si,j的最快時間,則f* = min(f1[n]+x1,f2[n]+x2)。逐步向下推導,直到j=1。
當j=1時: f1[1] = e1+a1,1,f2[1] = e2+a2,1。
當j>1時:f1[j] = min(f1[j-1]+a1,j,f2[j-1]+t2,j-1+a1,j),f2[j] = min(f2[j-1]+a2,j,f1[j-1]+t1,j-1+a2,j)。
(3)計算最快時間
有了遞歸的解,就能夠按照上述的思路編寫程序實現,爲了不用遞歸實現,須要開闢輔助空間來進行,以空間來換取時間,用C語言實現以下所示:
void fastest_way(int a[][N],int t[][N-1],int e[],int x[],int f[][N],int l[][N],int n) { int i,j; f[0][0] = e[0] + a[0][0]; f[1][0] = e[1] + a[1][0]; l[0][0] = 1; l[1][0] = 2; for(j=1;j<n;j++) { if(f[0][j-1] < f[1][j-1] + t[1][j-1]) { f[0][j] = f[0][j-1] + a[0][j]; l[0][j] = 1; } else { f[0][j] = f[1][j-1] + t[1][j-1] + a[0][j]; l[0][j] = 2; } if(f[1][j-1] < f[0][j-1] + t[0][j-1]) { f[1][j] = f[1][j-1] + a[1][j]; l[1][j] = 2; } else { f[1][j] = f[0][j-1] + t[0][j-1] + a[1][j]; l[1][j] = 1; } } if(f[0][n-1] + x[0] < f[1][n-1] + x[1]) { last_f = f[0][n-1] + x[0]; last_l = 1; } else { last_f = f[1][n-1] + x[1]; last_l = 2; } }
(4)構造經過工廠的最快線路
有第三步驟已經計算出來並記錄了每一個裝配站所在的裝配線編號,故能夠按照以站號遞減順序直接輸出,程序以下所示:
void print_station(int l[][N],int last_l,int n) { int i = last_l; int j; printf("line %d,station %d\n",i,n); for(j=n-1;j>0;--j) { i = l[i-1][j]; printf("line %d,station %d\n",i,j); } }
如果按照站號遞增順序輸出,則需經過遞歸進行實現,程序以下所示:
void print_station_recursive(int l[][N],int last_l,int n) { int i = last_l; if(n == 1) printf("line %d,station %d\n",i,n); else { print_station_recursive(l,l[i-1][n-1],n-1); printf("line %d,station %d\n",i,n); } }
四、編程實現
根據上面的分析,採用C語言實現以下:
#include <stdio.h> #include <stdlib.h> #define N 6 void fastest_way(int a[][N],int t[][N-1],int e[],int x[],int f[][N],int l[][N],int n); void print_station(int l[][N],int last_l,int n); void print_station_recursive(); //全局變量,last_t表示最短期,last_l表示最後一個裝配站所在的裝配線編號 int last_f,last_l; int main() { int a[2][6] = {{7,9,3,4,8,4},{8,5,6,4,5,7}}; int t[2][5] = {{2,3,1,3,4},{2,1,2,2,1}}; int f[2][6] = {0}; int l[2][6] = {0}; int e[2] = {2,4}; int x[2] = {3,2}; int i,j; fastest_way(a,t,e,x,f,l,6); //打印輸出各個裝配線上各個裝配站執行的最短期 for(i=0;i<2;++i) { printf("f%d is: ",i+1); for(j=0;j<6;++j) printf("%d ",f[i][j]); printf("\n"); } printf("last_f is: %d\nlast_l is: %d\n",last_f,last_l); for(i=0;i<2;++i) { printf("l%d is: ",i+1); for(j=0;j<6;++j) printf("%d ",l[i][j]); printf("\n"); } print_station(l,last_l,6); printf("output sequence by recursive.\n"); print_station_recursive(l,last_l,6); return 0; } void fastest_way(int a[][N],int t[][N-1],int e[],int x[],int f[][N],int l[][N],int n) { int i,j; f[0][0] = e[0] + a[0][0]; f[1][0] = e[1] + a[1][0]; l[0][0] = 1; l[1][0] = 2; for(j=1;j<n;j++) { if(f[0][j-1] < f[1][j-1] + t[1][j-1]) { f[0][j] = f[0][j-1] + a[0][j]; l[0][j] = 1; } else { f[0][j] = f[1][j-1] + t[1][j-1] + a[0][j]; l[0][j] = 2; } if(f[1][j-1] < f[0][j-1] + t[0][j-1]) { f[1][j] = f[1][j-1] + a[1][j]; l[1][j] = 2; } else { f[1][j] = f[0][j-1] + t[0][j-1] + a[1][j]; l[1][j] = 1; } } if(f[0][n-1] + x[0] < f[1][n-1] + x[1]) { last_f = f[0][n-1] + x[0]; last_l = 1; } else { last_f = f[1][n-1] + x[1]; last_l = 2; } } void print_station(int l[][N],int last_l,int n) { int i = last_l; int j; printf("line %d,station %d\n",i,n); for(j=n-1;j>0;--j) { i = l[i-1][j]; printf("line %d,station %d\n",i,j); } } void print_station_recursive(int l[][N],int last_l,int n) { int i = last_l; if(n == 1) printf("line %d,station %d\n",i,n); else { print_station_recursive(l,l[i-1][n-1],n-1); printf("line %d,station %d\n",i,n); } }
程序執行結果以下所示:
五、總結
動態規劃是個很是有效的設計方法,要善於用動態規劃去分析問題,重點是如何發現子問題的結構。最優子結構在問題域中以兩種方式變化(在找出這兩個問題的解以後,構造出原問題的最優子結構每每就不是難事了):
a) 有多少個子問題被用在原問題的一個最優解中
b) 在決定一個最優解中使用哪些子問題有多少個選擇
第十六章:動態規劃--矩陣鏈乘法
前言:今天接着學習動態規劃算法,學習如何用動態規劃來分析解決矩陣鏈乘問題。首先回顧一下矩陣乘法運算法,並給出C++語言實現過程。而後採用動態規劃算法分析矩陣鏈乘問題並給出C語言實現過程。
#include <iostream> using namespace std; #define A_ROWS 3 #define A_COLUMNS 2 #define B_ROWS 2 #define B_COLUMNS 3 void matrix_multiply(int A[A_ROWS][A_COLUMNS],int B[B_ROWS][B_COLUMNS],int C[A_ROWS][B_COLUMNS]); int main() { int A[A_ROWS][A_COLUMNS] = {1,0, 1,2, 1,1}; int B[B_ROWS][B_COLUMNS] = {1,1,2, 2,1,2}; int C[A_ROWS][B_COLUMNS] = {0}; matrix_multiply(A,B,C); for(int i=0;i<A_ROWS;i++) { for(int j=0;j<B_COLUMNS;j++) cout<<C[i][j]<<" "; cout<<endl; } return 0; } void matrix_multiply(int A[A_ROWS][A_COLUMNS],int B[B_ROWS][B_COLUMNS],int C[A_ROWS][B_COLUMNS]) { if(A_COLUMNS != B_ROWS) cout<<"error: incompatible dimensions."<<endl; else { int i,j,k; for(i=0;i<A_ROWS;i++) for(j=0;j<B_COLUMNS;j++) { C[i][j] = 0; for(k=0;k<A_COLUMNS;k++) C[i][j] += A[i][k] * B[k][j]; //將A的每一行的每一列與B的每一列的每一行的乘積求和 } } }
程序測試結果以下所示:
二、矩陣鏈乘問題描述
給定n個矩陣構成的一個鏈<A1,A2,A3,.......An>,其中i=1,2,...n,矩陣A的維數爲pi-1pi,對乘積 A1A2...An 以一種最小化標量乘法次數的方式進行加所有括號。
注意:在矩陣鏈乘問題中,實際上並無把矩陣相乘,目的是肯定一個具備最小代價的矩陣相乘順序。找出這樣一個結合順序使得相乘的代價最低。
三、動態規劃分析過程
1)最優加所有括號的結構
動態規劃第一步是尋找一個最優的子結構。假設如今要計算AiAi+1....Aj的值,計算Ai...j過程中確定會存在某個k值(i<=k<j)將Ai...j分紅兩部分,使得Ai...j的計算量最小。分紅兩個子問題Ai...k和Ak+1...j,須要繼續遞歸尋找這兩個子問題的最優解。
有分析能夠到最優子結構爲:假設AiAi+1....Aj的一個最優加全括號把乘積在Ak和Ak+1之間分開,則Ai..k和Ak+1..j也都是最優加全括號的。
2)一個遞歸解
設m[i,j]爲計算機矩陣Ai...j所需的標量乘法運算次數的最小值,對此計算A1..n的最小代價就是m[1,n]。如今須要來遞歸定義m[i,j],分兩種狀況進行討論以下:
當i==j時:m[i,j] = 0,(此時只包含一個矩陣)
當i<j 時:從步驟1中須要尋找一個k(i≤k<j)值,使得m[i,j] =min{m[i,k]+m[k+1,j]+pi-1pkpj} (i≤k<j)。
3)計算最優代價
雖然給出了遞歸解的過程,可是在實現的時候不採用遞歸實現,而是藉助輔助空間,使用自底向上的表格進行實現。設矩陣Ai的維數爲pi-1pi,i=1,2.....n。輸入序列爲:p=<p0,p1,...pn>,length[p] = n+1。使用m[n][n]保存m[i,j]的代價,s[n][n]保存計算m[i,j]時取得最優代價處k的值,最後能夠用s中的記錄構造一個最優解。書中給出了計算過程的僞代碼,摘錄以下:
MAXTRIX_CHAIN_ORDER(p) n = length[p]-1; for i=1 to n do m[i][i] = 0; for t = 2 to n //t is the chain length do for i=1 to n-t+1 j=i+t-1; m[i][j] = MAXLIMIT; for k=i to j-1 q = m[i][k] + m[k+1][i] + qi-1qkqj; if q < m[i][j] then m[i][j] = q; s[i][j] = k; return m and s;
MATRIX_CHAIN_ORDER具備循環嵌套,深度爲3層,運行時間爲O(n3)。若是採用遞歸進行實現,則須要指數級時間Ω(2n),由於中間有些重複計算。遞歸是徹底按照第二步獲得的遞歸公式進行計算,遞歸實現以下所示:
int recursive_matrix_chain(int *p,int i,int j,int m[N+1][N+1],int s[N+1][N+1]) { if(i==j) m[i][j] = 0; else { int k; m[i][j] = MAXVALUE; for(k=i;k<j;k++) { int temp = recursive_matrix_chain(p,i,k,m,s) +recursive_matrix_chain(p,k+1,j,m,s) + p[i-1]*p[k]*p[j]; if(temp < m[i][j]) { m[i][j] = temp; s[i][j] = k; } } } return m[i][j]; }
對遞歸算計的改進,能夠引入備忘錄,採用自頂向下的策略,維護一個記錄了子問題的表,控制結構像遞歸算法。完整程序以下所示:
int memoized_matrix_chain(int *p,int m[N+1][N+1],int s[N+1][N+1]) { int i,j; for(i=1;i<=N;++i) for(j=1;j<=N;++j) { m[i][j] = MAXVALUE; } return lookup_chain(p,1,N,m,s); } int lookup_chain(int *p,int i,int j,int m[N+1][N+1],int s[N+1][N+1]) { if(m[i][j] < MAXVALUE) return m[i][j]; //直接返回,至關於查表 if(i == j) m[i][j] = 0; else { int k; for(k=i;k<j;++k) { int temp = lookup_chain(p,i,k,m,s)+lookup_chain(p,k+1,j,m,s) + p[i-1]*p[k]*p[j]; //經過遞歸的形式計算,只計算一次,第二次查表獲得 if(temp < m[i][j]) { m[i][j] = temp; s[i][j] = k; } } } return m[i][j]; }
4)構造一個最優解
第三步中已經計算出來最小代價,並保存了相關的記錄信息。所以只需對s表格進行遞歸調用展開既能夠獲得一個最優解。書中給出了僞代碼,摘錄以下:
PRINT_OPTIMAL_PARENS(s,i,j) if i== j then print "Ai" else print "("; PRINT_OPTIMAL_PARENS(s,i,s[i][j]); PRINT_OPTIMAL_PARENS(s,s[i][j]+1,j); print")";
四、編程實現
採用C++語言實現這個過程,現有矩陣A1(30×35)、A2(35×15)、A3(15×5)、A4(5×10)、A5(10×20)、A6(20×25),獲得p=<30,35,15,5,10,20,25>。實現過程定義兩個二維數組m和s,爲了方便計算其第一行和第一列都忽略,行標和列標都是1開始。完整的程序以下所示:
#include <iostream> using namespace std; #define N 6 #define MAXVALUE 1000000 void matrix_chain_order(int *p,int len,int m[N+1][N+1],int s[N+1][N+1]); void print_optimal_parents(int s[N+1][N+1],int i,int j); int main() { int p[N+1] = {30,35,15,5,10,20,25}; int m[N+1][N+1]={0}; int s[N+1][N+1]={0}; int i,j; matrix_chain_order(p,N+1,m,s); cout<<"m value is: "<<endl; for(i=1;i<=N;++i) { for(j=1;j<=N;++j) cout<<m[i][j]<<" "; cout<<endl; } cout<<"s value is: "<<endl; for(i=1;i<=N;++i) { for(j=1;j<=N;++j) cout<<s[i][j]<<" "; cout<<endl; } cout<<"The result is:"<<endl; print_optimal_parents(s,1,N); return 0; } void matrix_chain_order(int *p,int len,int m[N+1][N+1],int s[N+1][N+1]) { int i,j,k,t; for(i=0;i<=N;++i) m[i][i] = 0; for(t=2;t<=N;t++) //當前鏈乘矩陣的長度 { for(i=1;i<=N-t+1;i++) //從第一矩陣開始算起,計算長度爲t的最少代價 { j=i+t-1;//長度爲t時候的最後一個元素 m[i][j] = MAXVALUE; //初始化爲最大代價 for(k=i;k<=j-1;k++) //尋找最優的k值,使得分紅兩部分k在i與j-1之間 { int temp = m[i][k]+m[k+1][j] + p[i-1]*p[k]*p[j]; if(temp < m[i][j]) { m[i][j] = temp; //記錄下當前的最小代價 s[i][j] = k; //記錄當前的括號位置,即矩陣的編號 } } } } } //s中存放着括號當前的位置 void print_optimal_parents(int s[N+1][N+1],int i,int j) { if( i == j) cout<<"A"<<i; else { cout<<"("; print_optimal_parents(s,i,s[i][j]); print_optimal_parents(s,s[i][j]+1,j); cout<<")"; } }
程序測試結果以下所示:
五、總結
動態規劃解決問題關鍵是分析過程,難度在於如何發現其子問題的結構及子問題的遞歸解。這個須要多多思考,不是短期內能明白。在實現過程當中遇到問題就是數組,數組的下標問題是個比較麻煩的事情,如何可以過合理的去處理,須要必定的技巧。
第十五章:動態規劃--最長公共子序列
一、基本概念
一個給定序列的子序列就是該給定序列中去掉零個或者多個元素的序列。形式化來說就是:給定一個序列X={x1,x2,……,xm},另一個序列Z={z1、z2、……,zk},若是存在X的一個嚴格遞增小標序列<i1,i2……,ik>,使得對全部j=1,2,……k,有xij = zj,則Z是X的子序列。例如:Z={B,C,D,B}是X={A,B,C,B,D,A,B}的一個子序列,相應的小標爲<2,3,5,7>。從定義能夠看出子序列直接的元素不必定是相鄰的。
公共子序列:給定兩個序列X和Y,若是Z既是X的一個子序列又是Y的一個子序列,則稱序列Z是X和Y的公共子序列。例如:X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A},則序列{B,C,A}是X和Y的一個公共子序列,但不不是最長公共子序列。
最長公共子序列(LCS)問題描述:給定兩個序列X={x1,x2,……,xm}和Y={y1,y2,……,yn},找出X和Y的最長公共子序列。
二、動態規劃解決過程
1)描述一個最長公共子序列
若是序列比較短,能夠採用蠻力法枚舉出X的全部子序列,而後檢查是不是Y的子序列,並記錄所發現的最長子序列。若是序列比較長,這種方法須要指數級時間,不切實際。
LCS的最優子結構定理:設X={x1,x2,……,xm}和Y={y1,y2,……,yn}爲兩個序列,並設Z={z1、z2、……,zk}爲X和Y的任意一個LCS,則:
(1)若是xm=yn,那麼zk=xm=yn,並且Zk-1是Xm-1和Yn-1的一個LCS。
(2)若是xm≠yn,那麼zk≠xm蘊含Z是是Xm-1和Yn的一個LCS。
(3)若是xm≠yn,那麼zk≠yn蘊含Z是是Xm和Yn-1的一個LCS。
定理說明兩個序列的一個LCS也包含兩個序列的前綴的一個LCS,即LCS問題具備最優子結構性質。
2)一個遞歸解
根據LCS的子結構可知,要找序列X和Y的LCS,根據xm與yn是否相等進行判斷的,若是xm=yn則產生一個子問題,不然產生兩個子問題。設C[i,j]爲序列Xi和Yj的一個LCS的長度。若是i=0或者j=0,即一個序列的長度爲0,則LCS的長度爲0。LCS問題的最優子結構的遞歸式以下所示:
3)計算LCS的長度
採用動態規劃自底向上計算解。書中給出了求解過程LCS_LENGTH,以兩個序列爲輸入。將計算序列的長度保存到一個二維數組C[M][N]中,另外引入一個二維數組B[M][N]用來保存最優解的構造過程。M和N分別表示兩個序列的長度。該過程的僞代碼以下所示:
LCS_LENGTH(X,Y) m = length(X); n = length(Y); for i = 1 to m c[i][0] = 0; for j=1 to n c[0][j] = 0; for i=1 to m for j=1 to n if x[i] = y[j] then c[i][j] = c[i-1][j-1]+1; b[i][j] = '\'; else if c[i-1][j] >= c[i][j-1] then c[i][j] = c[i-1][j]; b[i][j] = '|'; else c[i][j] = c[i][j-1]; b[i][j] = '-'; return c and b
由僞代碼能夠看出LCS_LENGTH運行時間爲O(mn)。
4)構造一個LCS
根據第三步中保存的表b構建一個LCS序列。從b[m][n]開始,當遇到'\'時,表示xi=yj,是LCS中的一個元素。經過遞歸便可求出LCS的序列元素。書中給出了僞代碼以下所示:
PRINT_LCS(b,X,i,j) if i==0 or j==0 then return if b[i][j] == '\' then PRINT_LCS(b,X,i-1,j-1) print X[i] else if b[i][j] == '|' then PRINT_LCS(b,X,i-1,j) else PRINT_LSC(b,X,i,j-1)
三、編程實現
如今採用C++語言實現上述過程,例若有兩個序列X={A,B,C,B,D,A,B}和Y={B,D,C,A,B,A},求其最長公共子序列Z。完整程序以下所示:
#include <iostream> using namespace std; #define X_LEN 7 #define Y_LEN 6 #define EQUAL 0 #define UP 1 #define LEVEL 2 void lcs_length(char* X,char* Y,int c[X_LEN+1][Y_LEN+1],int b[X_LEN+1][Y_LEN+1]); void print_lcs(int b[X_LEN+1][Y_LEN+1],char *X,int i,int j); int main() { char X[X_LEN+1] = {' ','A','B','C','B','D','A','B'}; char Y[Y_LEN+1] = {' ','B','D','C','A','B','A'}; int c[X_LEN+1][Y_LEN+1]={0}; int b[X_LEN+1][Y_LEN+1] = {0}; int i,j; lcs_length(X,Y,c,b); for(i=0;i<=X_LEN;i++) { for(j=0;j<=Y_LEN;j++) cout<<c[i][j]<<" "; cout<<endl; } cout<<"The length of LCS is: "<<c[X_LEN][Y_LEN]<<endl; cout<<"The longest common subsequence between X and y is: "<<endl; print_lcs(b,X,X_LEN,Y_LEN); return 0; } //採用動態規劃方法自底向上的進行計算,尋找最優解 void lcs_length(char* X,char* Y,int c[X_LEN+1][Y_LEN+1],int b[X_LEN+1][Y_LEN+1]) { int i,j; //設置邊界條件,即i=0或者j=0 for(i=0;i<X_LEN;i++) c[i][0] = 0; for(j=0;j<Y_LEN;j++) c[0][j] = 0; for(i=1;i<=X_LEN;i++) for(j=1;j<=Y_LEN;j++) { if(X[i] == Y[j]) //知足遞歸公式第二條 { c[i][j] = c[i-1][j-1]+1; b[i][j] = EQUAL ; } else if(c[i-1][j] >= c[i][j-1]) //遞歸公式第三條 { c[i][j] = c[i-1][j]; b[i][j] = UP; } else { c[i][j] = c[i][j-1]; b[i][j] = LEVEL; } } } void print_lcs(int b[X_LEN+1][Y_LEN+1],char *X,int i,int j) { if(i==0 || j==0) return; if(b[i][j] == EQUAL) { print_lcs(b,X,i-1,j-1); cout<<X[i]<<" "; } else if(b[i][j] == UP) print_lcs(b,X,i-1,j); else print_lcs(b,X,i,j-1); }
程序測試結果以下所示:
第十五章:動態規劃--最優二叉查找樹
一、前言:
接着學習動態規劃方法,最優二叉查找樹問題。二叉查找樹參考http://www.cnblogs.com/Anker/archive/2013/01/28/2880581.html。若是在二叉樹中查找元素不考慮機率及查找不成功的狀況下,能夠採用紅黑樹或者平衡二叉樹來搜索,這樣能夠在O(lgn)時間內完成。而現實生活中,查找的關鍵字是有必定的機率的,就是說有的關鍵字可能常常被搜索,而有的不多被搜索,並且搜索的關鍵字可能不存在,爲此須要根據關鍵字出現的機率構建一個二叉樹。好比中文輸入法字庫中各詞條(單字、詞組等)的先驗機率,針對用戶習慣能夠自動調整詞頻——所謂動態調頻、高頻先現原則,以減小用戶翻查次數,使得常常用的詞彙被放置在前面,這樣就能有效地加快查找速度。這就是最優二叉樹所要解決的問題。
二、問題描述
給定一個由n個互異的關鍵字組成的有序序列K={k1<k2<k3<,……,<kn}和它們被查詢的機率P={p1,p2,p3,……,pn},要求構造一棵二叉查找樹T,使得查詢全部元素的總的代價最小。對於一個搜索樹,當搜索的元素在樹內時,表示搜索成功。當不在樹內時,表示搜索失敗,用一個「虛葉子節點」來標示搜索失敗的狀況,所以須要n+1個虛葉子節點{d0<d1<……<dn},對於應di的機率序列是Q={q0,q1,……,qn}。其中d0表示搜索元素小於k1的失敗結果,dn表示搜索元素大於kn的失敗狀況。di(0<i<n)表示搜索節點在ki和k(i+1)之間時的失敗狀況。所以有以下公式:
由每一個關鍵字和每一個虛擬鍵被搜索的機率,能夠肯定在一棵給定的二叉查找樹T內一次搜索的指望代價。設一次搜索的實際代價爲檢查的節點個數,即在T內搜索所發現的節點的深度加上1。因此在T內一次搜索的指望代價爲:
須要注意的是:一棵最優二叉查找樹不必定是一棵總體高度最小的樹,也不必定老是把最大機率的關鍵字放在根部。
(3)動態規劃求解過程
1)最優二叉查找樹的結構
若是一棵最優二叉查找樹T有一棵包含關鍵字ki,……,kj的子樹T',那麼這棵子樹T’對於對於關鍵字ki,……kj和虛擬鍵di-1,……,dj的子問題也一定是最優的。
2)一個遞歸解
定義e[i,j]爲搜索一棵包含關鍵字ki,……,kj的最優二叉查找樹的指望代價,則分類討論以下:
當j=i-1時,說明此時只有虛擬鍵di-1,故e[i,i-1] = qi-1
當j≥i時,須要從ki,……,kj中選擇一個跟kr,而後用關鍵字ki,……,kr-1來構造一棵最優二叉查找樹做爲左子樹,用關鍵字kr+1,……,kj來構造一棵最優二叉查找樹做爲右子樹。定義一棵有關鍵字ki,……,kj的子樹,定義機率的總和爲:
所以若是kr是一棵包含關鍵字ki,……,kj的最優子樹的根,則有:
故e[i,j]重寫爲:
最終的遞歸式以下:
3)計算一棵最優二叉查找樹的指望搜索代價
將e[i,j]的值保存到一個二維數組e[1..1+n,0..n]中,用root[i,j]來記錄關鍵字ki,……,kj的子樹的根,採用二維數組root[1..n,1..n]來表示。爲了提升效率,防止重複計算,須要個二維數組w[1..n+1,0...n]來保存w(i,j)的值,其中w[i,j] = w[i,j-1]+pj+qj。數組給出了計算過程的僞代碼:
OPTIMAL_BST(p,q,n) for i=1 to n+1 //初始化e和w的值 do e[i,i-1] = qi-1; w[i,i-1] = qi-1; for l=1 to n do for i=1 to n-l+1 do j=i+l-1; e[i,j] = MAX; w[i,j] = w[i,j-1]+pj+qj; for r=i to j do t=e[i,r-1]+e[r+1,j]+w[i,j] if t<e[i,j] then e[i,j] = t; root[i,j] = r; return e and root;
4)構造一棵最優二叉查找樹
根據地第三步中獲得的root表,能夠遞推出各個子樹的根,從而能夠構建出一棵最優二叉查找樹。從root[1,n]開始向下遞推,一次找出樹根,及左子樹和右子樹。
四、編程實現
針對一個具體的實例編程實現,如今有5個關鍵字,其出現的機率P={0.15,0.10,0.05,0.10,0.20},查找虛擬鍵的機率q={0.05,0.10,0.05,0.05,0.05,0.10}。採用C++語言是實現以下:
#include <iostream> using namespace std; #define N 5 #define MAX 999999.99999 void optimal_binary_search_tree(float *p,float *q,int n,float e[N+2][N+1],int root[N+1][N+1]); void construct_optimal_bst1(int root[N+1][N+1],int i,int j); void construct_optimal_bst2(int root[N+1][N+1],int i,int j); int main() { float p[N+1] = {0,0.15,0.10,0.05,0.10,0.20}; float q[N+1] = {0.05,0.10,0.05,0.05,0.05,0.10}; float e[N+2][N+1]; int root[N+1][N+1]; int i,j; optimal_binary_search_tree(p,q,N,e,root); cout<<"各個子樹的指望代價以下所示:"<<endl; for(i=1;i<=N+1;i++) { for(j=i-1;j<=N;j++) cout<<e[i][j]<<" "; cout<<endl; } cout<<"最優二叉查找樹的代價爲: "<<e[1][N]<<endl; cout<<"各個子樹根以下表所示:"<<endl; for(i=1;i<=N;i++) { for(j=i;j<=N;j++) cout<<root[i][j]<<" "; cout<<endl; } cout<<"構造的最優二叉查找樹以下所示:"<<endl; construct_optimal_bst1(root,1,N); cout<<"\n最優二叉查找樹的結構描述以下:"<<endl; construct_optimal_bst2(root,1,N); cout<<endl; return 0; } void optimal_binary_search_tree(float *p,float *q,int n,float e[N+2][N+1],int root[N+1][N+1]) { int i,j,k,r; float t; float w[N+2][N+1]; for(i=1;i<=N+1;++i) //主表和根表元素的初始化 { e[i][i-1] = q[i-1]; w[i][i-1] = q[i-1]; } for(k=1;k<=n;++k) //自底向上尋找最優子樹 for(i=1;i<=n-k+1;i++) { j = i+k-1; e[i][j] = MAX; w[i][j] = w[i][j-1]+p[j]+q[j]; for(r=i;r<=j;r++) //找最優根 { t = e[i][r-1] + e[r+1][j] +w[i][j]; if(t < e[i][j]) { e[i][j] = t; root[i][j] = r; } } } } void construct_optimal_bst1(int root[N+1][N+1],int i,int j) { if(i<=j) { int r = root[i][j]; cout<<r<<" "; construct_optimal_bst1(root,i,r-1); construct_optimal_bst1(root,r+1,j); } } void construct_optimal_bst2(int root[N+1][N+1],int i,int j) { if(i==1 && j== N) cout<<"k"<<root[1][N]<<"是根"<<endl; if(i<j) { int r = root[i][j]; if(r != i) cout<<"k"<<root[i][r-1]<<"是k"<<r<<"的左孩子"<<endl; construct_optimal_bst2(root,i,r-1); if(r!= j) cout<<"k"<<root[r+1][j]<<"是k"<<r<<"的右孩子"<<endl; construct_optimal_bst2(root,r+1,j); } if(i==j) { cout<<"d"<<i-1<<"是k"<<i<<"左孩子"<<endl; cout<<"d"<<i<<"是k"<<i<<"右孩子"<<endl; } if(i>j) cout<<"d"<<j<<"是k"<<j<<"右孩子"<<endl; }
程序測試結果以下所示:
動態規劃方法之生成最優二叉查找樹
一、概念引入
六、具體實現代碼(其中全部數據都存放在2.txt中,其內容爲:
其中5表示有5個節點,其餘數據表示各個節點出現的機率;
#include<stdio.h> #include<stdlib.h> #define max 9999 void OptimalBST(int,float*,float**,int**); void OptimalBSTPrint(int,int,int**); void main() { int i,num; FILE *point; //全部數據均從2.txt中獲取,2.txt中第一個數據表示節點個數;從第二個數據開始表示各個節點的機率 point=fopen("2.txt","r"); if(point==NULL) { printf("cannot open 2.txt.\n"); exit(-1); } fscanf(point,"%d",&num); printf("%d\n",num); float *p=(float*)malloc(sizeof(float)*(num+1)); for(i=1;i<num+1;i++) fscanf(point,"%f",&p[i]); //建立主表; float **c=(float**)malloc(sizeof(float*)*(num+2)); for(i=0;i<num+2;i++) c[i]=(float*)malloc(sizeof(float)*(num+1)); //建立根表; int **r=(int**)malloc(sizeof(int*)*(num+2)); for(i=0;i<num+2;i++) r[i]=(int*)malloc(sizeof(int)*(num+1)); //動態規劃實現最優二叉查找樹的指望代價求解。。 OptimalBST(num,p,c,r); printf("該最優二叉查找樹的指望代價爲:%f \n",c[1][num]); //給出最優二叉查找樹的中序遍歷結果; printf("構形成的最優二叉查找樹的中序遍歷結果爲:"); OptimalBSTPrint(1,4,r); } void OptimalBST(int num,float*p,float**c,int**r) { int d,i,j,k,s,kmin; float temp,sum; for(i=1;i<num+1;i++)//主表和根表元素的初始化 { c[i][i-1]=0; c[i][i]=p[i]; r[i][i]=i; } c[num+1][num]=0; for(d=1;d<=num-1;d++)//加入節點序列 { for(i=1;i<=num-d;i++) { j=i+d; temp=max; for(k=i;k<=j;k++)//找最優根 { if(c[i][k-1]+c[k+1][j]<temp) { temp=c[i][k-1]+c[k+1][j]; kmin=k; } } r[i][j]=kmin;//記錄最優根 sum=p[i]; for(s=i+1;s<=j;s++) sum+=p[s]; c[i][j]=temp+sum; } } } //採用遞歸方式實現最優根的輸出,最優根都是保存在r[i][j]中的。。。 void OptimalBSTPrint(int first,int last,int**r) { int k; if(first<=last) { k=r[first][last]; printf("%d ",k); OptimalBSTPrint(first,k-1,r); OptimalBSTPrint(k+1,last,r); } }
七、最終運行結果:
八、參考文獻:
(1)算法導論
(2)數據結構 嚴蔚敏
第十五章:動態規劃總結
前言:
書中列舉四個常見問題,分析如何採用動態規劃方法進行解決。今天把動態規劃算法總結一下。關於四個問題的動態規範分析過程能夠參考前面的幾篇日誌,連接以下:
裝配線調度問題:http://www.cnblogs.com/Anker/archive/2013/03/09/2951785.html
矩陣鏈乘問題:http://www.cnblogs.com/Anker/archive/2013/03/10/2952475.html
最長公共子序列問題:http://www.cnblogs.com/Anker/archive/2013/03/11/2954050.html
最優二叉查找樹問題:http://www.cnblogs.com/Anker/archive/2013/03/13/2958488.html
一、基本概念
動態規劃是經過組合子問題的解而解決整個問題的,經過將問題分解爲相互不獨立(各個子問題包含有公共的子問題,也叫重疊子問題)的子問題,對每一個子問題求解一次,將其結果保存到一張輔助表中,避免每次遇到各個子問題時從新計算。動態規劃一般用於解決最優化問題,其設計步驟以下:
(1)描述最優解的結構。
(2)遞歸定義最優解的值。
(3)按自底向上的方式計算最優解的值。
(4)由計算出的結果構造出一個最優解。
第一步是選擇問題的在何時會出現最優解,經過分析子問題的最優解而達到整個問題的最優解。在第二步,根據第一步獲得的最優解描述,將整個問題分紅小問題,直到問題不可再分爲止,層層選擇最優,構成整個問題的最優解,給出最優解的遞歸公式。第三步根據第二步給的遞歸公式,採用自底向上的策略,計算每一個問題的最優解,並將結果保存到輔助表中。第四步驟是根據第三步中的最優解,藉助保存在表中的值,給出最優解的構造過程。
動態規劃與分治法之間的區別:
(1) 分治法是指將問題分紅一些獨立的子問題,遞歸的求解各子問題。
(2) 動態規劃適用於這些子問題不是獨立的狀況,也就是各子問題包含公共子問題。
二、動態規劃基礎
何時可使用動態規範方法解決問題呢?這個問題須要討論一下,書中給出了採用動態規範方法的最優化問題中的兩個要素:最優子結構和重疊子結構。
1)最優子結構
最優子結構是指問題的一個最優解中包含了其子問題的最優解。在動態規劃中,每次採用子問題的最優解來構造問題的一個最優解。尋找最優子結構,遵循的共同的模式:
(1)問題的一個解能夠是作一個選擇,獲得一個或者多個有待解決的子問題。
(2)假設對一個給定的問題,已知的是一個能夠致使最優解的選擇,沒必要關心如何肯定這個選擇。
(3)在已知這個選擇後,要肯定哪些子問題會隨之發生,如何最好地描述所獲得的子問題空間。
(4)利用「剪貼」技術,來證實問題的一個最優解中,使用的子問題的解自己也是最優的。
最優子結構在問題域中以兩種方式變化:
(1)有多少個子問題被使用在原問題的一個最優解中。
(2)在決定一個最優解中使用哪些子問題時有多少個選擇。
動態規劃按照自底向上的策略利用最優子結構,即:首先找到子問題的最優解,解決子問題,而後逐步向上找到問題的一個最優解。爲了描述子問題空間,能夠遵循這樣一條有效的經驗規則,就是儘可能保持這個空間簡單,而後在須要時再擴充它。
注意:在不能應用最優子結構的時候,就必定不能假設它可以應用。 警戒使用動態規劃去解決缺少最優子結構的問題!
使用動態規劃時,子問題之間必須是相互獨立的!能夠這樣理解,N個子問題域互不相干,屬於徹底不一樣的空間。
2)重疊子問題
用來解決原問題的遞歸算法能夠反覆地解一樣的子問題,而不是老是產生新的子問題。重疊子問題是指當一個遞歸算法不斷地調用同一個問題。動態規劃算法老是充分利用重疊子問題,經過每一個子問題只解一次,把解保存在一個須要時就能夠查看的表中,每次查表的時間爲常數。
由計算出的結果反向構造一個最優解:把動態規劃或者是遞歸過程當中做出的每一次選擇(記住:保存的是每次做出的選擇)都保存下來,在最後就必定能夠經過這些保存的選擇來反向構造出最優解。
作備忘錄的遞歸方法:這種方法是動態規劃的一個變形,它本質上與動態規劃是同樣的,可是比動態規劃更好理解!
(1) 使用普通的遞歸結構,自上而下的解決問題。
(2) 當在遞歸算法的執行中每一次遇到一個子問題時,就計算它的解並填入一個表中。之後每次遇到該子問題時,只要查看並返回表中先前填入的值便可。
三、總結
動態規劃的核心就是找到問題的最優子結構,在找到最優子結構以後的消除重複子問題。最終不管是採用動態規劃的自底向上的遞推,仍是備忘錄,或者是備忘錄的變型,均可以輕鬆的找出最優解的構造過程。