動態規劃(dynamic programming)是一種高效的程序設計技術,通常應用與最優化問題,即當咱們面臨多組選擇時,選擇一個可行解讓問題達到最優。動態規劃的一個顯著特色是:原問題能夠劃分紅更小的子問題的最優化問題,而這些子問題的解每每有着重疊的部分。ios
動態規劃算法的解決一個問題,能夠分紅四個步驟:算法
1)描述最優解結構,需找最優子結構編程
2)遞歸定義最優值的解數組
3)自底向上的求解問題函數
4)依據計算過程,構造一個最優解優化
1)與 2)是這個問題能夠用動態規劃解決的理論基礎,3)能夠看出是動態規劃算法的編程實踐,4)是算法輸出,依賴於3)。下面分別用三個經典的動態規劃案例,闡釋動態規劃算法的用法。spa
案例一:矩陣連乘設計
問題描述:給定n個矩陣{A1,A2,…,An},其中Ai與Ai+1是可乘的,i=1,2,…,n-1。考察這n個矩陣的連乘積A1A2…An。因爲矩陣乘法知足結合律,故計算矩陣的連乘積能夠有許多不一樣的計算次序,這種計算次序能夠用加括號的方式來肯定。若一個矩陣連乘積的計算次序徹底肯定,則能夠依這次序反覆調用2個矩陣相乘的標準算法計算出矩陣連乘積。若A是一個p×q矩陣,B是一個q×r矩陣,則計算其乘積C=AB的標準算法中,須要進行pqr次數乘。矩陣連乘積的計算次序不一樣,計算量也不一樣code
Example:先考察3個矩陣{A1,A2,A3}連乘,設這三個矩陣的維數分別爲10×100,100×5,5×50。blog
解決方案:動態規劃
1)描述最優解結構,尋找最優子結構
記AiAi+1…Aj爲A[i:j],考察A[1:n]的最優計算次序問題:假設這個計算次序在k(1<=k<=n)處斷開,那麼A[1:k]和A[k+1:n]兩個子序列的中的計算次序也是最優的。
爲證實子結構與原問題也是一個相同的最優問題,通常採用反證法:
若是A[1:k]或者A[k+1:n]不是最優的,那麼能夠必然能夠找到一個新的計算次序將A[1:k]或是A[k+1:n]替換,新的計算序列須要的計算次數更少,但這與A[1:n]是最有解序列矛盾。
2)遞歸定義最優值
令m[i][j]表示A[i:j]最小的計算次數,那麼遞歸定義的表達式以下:
3)自底向上的求解
有了2)的遞歸表達式,,代碼實現將會變得簡單,代碼以下:
/************************************************************************/ /* p: 輸入參數,存儲矩陣序列的中行列值 * m: m[i][j], 存放A[i:j]的計算次數 s: s[i][j], 記錄A[i:j]斷開的位置 */ /************************************************************************/ int matrix_chain(int* p, int n, int** m, int** s) { for (int i = 0; i != n; i++) { m[i][i] = 0; } for (int r = 2; r <= n; r++) { for (int i = 0; i <= n-r; i++) { int j = i + r - 1; m[i][j] = m[i+1][j] + p[i]*p[i+1]*p[j+1]; //從i處斷開 s[i][j] = i; for (int k = i+1; k <= j; k++) { int t = m[i][k] +m[k+1][j] + p[i]*p[k+1]*p[j+1]; //從k處斷開
if (t< m[i][j])
{
m[i][j] = t;
s[i][j] = k;
}
}
}
}
}
4)構造最優解
在第三步自底向上的求解過程當中,記錄了構建最優解的最優的必要形式(A[i:j]該斷開的位置),構造最優解的過程以下:
void trace_back(int** s, int i, int j) { if (i == j) { return; } trace_back(s, i, s[i][j]); trace_back(s, s[i][j] + 1, j); cout<<"A("<<i<<","<<s[i][j]<<")"<<"\t"; cout<<"Multiply\t"<<"A("<<s[i][j]+1<<","<<j<<")"<<endl; }
程序的主函數以下:
int main() { const int n = 6; int p[] = {30, 35, 15, 5, 10, 20, 25}; int **m, **s; m = new int*[n]; for( int i = 0; i < n; i++) m[i] = new int[n]; s = new int*[n]; for(int i=0; i<n; i++) s[i] = new int[n]; matrix_chain(p, n, m, s); trace_back(s, 0, n-1); for(int i=0;i<n;i++) { delete []m[i]; delete []s[i]; } delete []m; delete []s; system("pause"); return 0; }
案例二:最長公共子序列
問題描述:子序列是指,在原序列中刪除若干元素後所得的序列。公共子序列是指,給定兩個序列X和Y,另外一個序列Z既是X的子序列又是Y的子序列,那麼Z則被稱爲X與Y的公共子序列。而最長公共子序列,則是求X與Y最長的子序列中長度最大的一個序列。
Example: X = {A,B, C, B, D, A, B}, Y = {B, D, C, A, B, A},他們的一個最長公共子序列是Z = {B, C, B, A }
解決方案:動態規劃
步驟一:描述最優解結構,尋找最優子結構
設序列X = {x1, x2,....xn}, 序列Y = {y1, y2,...ym},它們的最長公共子序列是Z = {z1,z2, ...zk},他們之間有以下的最優結構性質:
1)若xn=ym,則zk=xn=ym,zk-1將是Xn-1與Yn-1的最長公共子序列
2)若xn=ym且zk≠xn,那麼Z是Y與Xn-1最長公共子序列
3)若xn=ym且zk≠ym,那麼Z是X與Yn-1最長公共子序列
有關最優子結構性質,依然能夠採用反證法證實。
2)遞歸定義最優值
記c[i][j]表示序列Xi與序列Yj最長公共子序列的長度,其中Xi = {x1, x2,....xi}, Yj = {y1, y2,...yj},有以下的遞歸定義表達式:
3)自底向上的求解
依照上面的公式,自底向上的求解代碼的代碼以下:
#include <iostream> using namespace std; int lcs_length(char* x, int m, char* y, int n, int** c) { for (int i = 0; i <= m ; i++) c[i][0] = 0; for (int j = 0; j <= n; j++) c[0][j] = 0; for (int i = 1; i <=m; i++) { for (int j = 1; j <= n; j++) { if (x[i-1] == y[j-1]) //下標從l開始 { c[i][j] = c[i-1][j-1] + 1; } else { c[i][j] = max(c[i-1][j], c[i][j-1]); } } } return c[m][n]; } void print_lcs(int i, int j, char* x, int** c) { if (i == 0 || j == 0) { return; } if (c[i][j] == c[i-1][j-1] + 1) { print_lcs(i-1, j-1, x, c); cout<<x[i-1]<<endl; } else if (c[i][j] == c[i-1][j]) { print_lcs(i-1, j, x, c); } else { print_lcs(i, j-1, x, c); } } int main() { char x[] = {"ABCBDAB"}; char y[] = {"BDCABA"}; int m = strlen(x); int n = strlen(y); int** c = new int*[m+1]; for (int i = 0; i<=m; i++ ) { c[i] = new int[n+1]; } int t = lcs_length(x, m, y, n, c); for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { cout<<c[i][j]<< " "; } cout<<endl; } print_lcs(m, n, x, c); system("pause"); return 0; }
代碼的執行圖解以下:
案例三:最長遞增子序列
問題描述:求一個一維數組(N個元素)中的最長遞增子序列的長度。
Example:在序列1,-1,2,-3,4,-5,6,-7中,其最長的遞增子序列爲1,2,4,6。
解決方案:動態規劃
繼續依據動態規劃的解決思想,解決過程省略,這裏直接給出最優解的遞歸結構表達式,令m(i, j)表示以i爲起點j爲終點(包括原始array[i])的子序列,增加序列的最長長度,則遞歸表達式爲:
實現代碼以下:
#include <iostream> using namespace std; /************************************************************************/ /* array: 存放序列 m: m[i][j],表示以i爲起點j爲終點(包括原始array[i])的子序列,增加序列長度 n: array長度 */ /************************************************************************/ int lis_length(int* array, int** m, int n) { for (int i = 0; i < n; i++) { m[i][i] = 1; } for (int r = 2; r <= n; r++) { for (int i = 0; i <= n - r; i++) { int j = i + r -1; if (array[i] < array[i+1]) { m[i][j] = m[i+1][j] + 1; } else { m[i][j] = m[i+1][j]; } } } return m[0][n-1]; } void print_lis(int* array, int** m, int n) { int lic_len = m[0][n-1]; //m[0][i]記錄了序列array[0:i],最長遞增加度 //m[0][i] = k,序列第k個增加元素出現 for (int i = 0, lic_tag = 1; i != n; i++) { if (m[0][i] == lic_tag) { cout<<array[i]<<" "; lic_tag += 1; } } } int main() { const int n = 8; int array[n] = {1, 4, 2, -3, 4, 8, 6, -7}; int** m = new int*[n]; for (int i = 0; i != n; i++) { m[i] = new int[n]; } int t = lis_length(array, m, n); print_lis(array, m, n-1); for (int i = 0; i != n; i++) { delete[] m[i]; } delete[] m; system("pause"); return 0; }
總結:1)可以最優子結構性質,是使用動態規劃算法的先決條件;2)重複解結構,動態規劃算法可以高效使用的緣由在於記錄了子結構的解,而這些解又會在後續的求解過程當中被咱們使用(優於遞歸算法的緣由);3)最後在編程實踐技巧上,利用了自底向上的求解技術,從子問題開始求解。
在編程實踐的過程,特別須要主要如下問題:1)初始化,零界狀況下表格的初始化必須提早完成;2)子問題的求解必定要現優於原問題,如出現某個子問題未求解出,但算法開始處理該原問題,將會引入錯誤(錯誤將很是隱晦);3)解的信息保存。