動態規劃算法一般用於求解具備某種最優性質的問題。在這類問題中,可能會有許多可行解。每個解都對應於一個值,咱們但願找到具備最優值的解。ios
將待求解問題分解成若干個子問題,先求解子問題,而後從這些子問題的解獲得原問題的解。適合於用動態規劃求解的問題,經分解獲得子問題每每不是互相獨立的。若用分治法來解這類問題,則分解獲得的子問題數目太多,有些子問題被重複計算了不少次。若是咱們可以保存已解決的子問題的答案,而在須要時再找出已求得的答案,這樣就能夠避免大量的重複計算,節省時間。爲了達到此目的,咱們能夠用一個表來記錄全部已解的子問題的答案。無論該子問題之後是否被用到,只要它被計算過,就將其結果填入表中。算法
一、最優子結構:數組
當問題的最優解包含了其子問題的最優解時,稱該問題具備最優子結構性質。spa
二、重疊子問題:3d
在用遞歸算法自頂向下解問題時,每次產生的子問題並不老是新問題,有些子問題被反覆計算屢次。動態規劃算法正是利用了這種子問題的重疊性質,對每個子問題只解一次,然後將其解保存在一個表格中,在之後儘量多地利用這些子問題的解。code
一、找出最優解的性質,並刻畫其結構特徵;blog
二、遞歸地定義最優值(寫出動態規劃方程);遞歸
三、以自底向上的方式計算出最優值;ci
四、根據計算最優值時獲得的信息,構造一個最優解。io
說明:(1)步驟 1~3 是動態規劃算法的基本步驟;
(2)在只須要求出最優值的情形,步驟 4 能夠省略;
(3)若須要求出問題的一個最優解,則必須執行步驟 4。
一、矩陣連乘問題
m × n 矩陣 A 與 n × p 矩陣 B 相乘需耗費的時間。咱們把 m x n x p 做爲兩個矩陣相乘所需時間的測量值。
如今假定要計算三個矩陣 A、B 和 C 的乘積,有兩種方式計算此乘積。
(1)先用 A 乘以 B 獲得矩陣 D,而後 D 乘以 C 獲得最終結果,這種乘法的順序爲(AB)C;
(2)先用 B 乘以 C 獲得矩陣 E,而後 E 乘以 A 獲得最終結果,這種乘法的順序爲 A(BC)。
儘管這兩種不一樣的計算順序所得的結果相同,但時間消耗會有很大的差距。
實例:
圖1.1 A、B 和 C 矩陣
矩陣乘法符合結合律,因此在計算 ABC 矩陣連乘時,有兩種方案,即 (AB)C 和 A(BC)。
對於第一方案(AB)C 和,計算:
圖1.2 AB 矩陣相乘
其乘法運算次數爲:2 × 3 × 2 = 12
圖1.3 (AB)C 矩陣連乘
其乘法運算次數爲:2 × 2 × 4 = 16
總計算量爲:12 + 16 = 28
對第二方案 A(BC),計算:
圖1.4 BC 矩陣相乘
其乘法運算次數爲:3 × 2 × 4 = 24
圖1.5 A、B 和 C 矩陣連乘
其乘法運算次數爲:2 × 3 × 4 = 24
總計算量爲:24 + 24 = 48
可見,不一樣方案的乘法運算量可能相差很懸殊。
問題定義:
給定 n 個矩陣 {A1, A2, …, An},其中 Ai 與 Ai+1 是可乘的,i = 1,2,…,n-1。考察這 n 個矩陣的連乘積 A1A2…An。 因爲矩陣乘法知足結合律,因此計算矩陣的連乘能夠有許多不一樣的計算次序。
這種計算次序能夠用加括號的方式來肯定。徹底加括號的矩陣連乘積可遞歸地定義爲:
(1)單個矩陣是徹底加括號的;
(2)矩陣連乘積 A 是徹底加括號的,則 A 可表示爲 2 個徹底加括號的矩陣連乘積 B 和 C 的乘積並加括號,即 A = (BC)。設有四個矩陣 A, B, C, D,總共有五種徹底加括號的方式: (A((BC)D)) , (A(B(CD))) , ((AB)(CD)) , (((AB)C)D) , ((A(BC)D))。
a. 找出最優解的性質,並刻畫其結構特徵;
將矩陣連乘積 AiAi+1…Aj ,簡記爲 A[i : j], 這裏 i≤j;考察計算 A[1:n] 的最優計算次序。
設這個計算次序在矩陣 Ak 和 Ak+1 之間將矩陣鏈斷開,1 ≤ k < n,則其相應徹底加括號方式爲 (A1A2…Ak)(Ak+1Ak+2…An)。
總計算量 = A[1:k] 的計算量 + A[k+1:n] 的計算量 + A[1:k] 和 A[k+1:n]相乘的計算量
特徵:計算 A[1:n] 的最優次序所包含的計算矩陣子鏈 A[1:k] 和 A[k+1:n] 的次序也是最優的。
b. 遞歸地定義最優值(寫出動態規劃方程);
圖1.6 創建遞歸關係
c. 以自底向上的方式計算出最優值。
1 #include <iostream> 2 using namespace std; 3 4 #define NUM 51 5 int p[NUM]; //矩陣維數 P0 x P1,P1 x P2,P2 x P3,...,P5 x P6 6 int m[NUM][NUM]; //最少乘次數 / 最優值數組 7 int s[NUM][NUM]; //最優斷開位置 8 9 void MatrixChain(int n) 10 { 11 for (int i = 1; i <= n; i++) m[i][i] = 0; 12 13 for (int r = 2; r <= n; r++) //矩陣個數 14 for (int i = 1; i <= n - r+1; i++) //起始 15 { 16 int j=i+r-1; //結尾 17 m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j]; //計算初值,從i處斷開,計算最優斷開位置 18 s[i][j] = i; 19 for (int k = i+1; k < j; k++) 20 { 21 int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j]; 22 if (t < m[i][j]) { m[i][j] = t; s[i][j] = k;} 23 } 24 } 25 } 26 27 void TraceBack(int i, int j) 28 { 29 if(i==j) 30 cout<< "A" << i; 31 else 32 { 33 cout << "("; 34 TraceBack(i, s[i][j]); 35 TraceBack(s[i][j]+1, j); 36 cout << ")"; 37 } 38 } 39 40 int main() 41 { 42 int n; 43 cin >> n; //矩陣的個數 44 int temp; 45 for(int i = 0; i < n; i++) 46 cin >> p[i] >> temp; //矩陣的維數 47 p[n] = temp; 48 MatrixChain(n); 49 cout << m[1][n] << endl; //最少乘次數 50 TraceBack(1, n); //按照最優斷開位置列出乘法順序 51 return 0; 52 }
二、矩陣連乘之備忘錄方法
備忘錄方法是動態規劃算法的變形。與動態規劃算法同樣,備忘錄方法用一個表格保存已解決的子問題的答案,再碰到該子問題時,只要簡單地查看該子問題的解答,而沒必要從新求解。備忘錄方法的控制結構與直接遞歸方法的控制結構相同,區別僅在於備忘錄方法爲每一個解過的子問題創建了備忘錄以備須要時查看,避免了相同子問題的重複求解。
1 #include <iostream> 2 using namespace std; 3 4 #define NUM 51 5 int p[NUM]; //矩陣維數 P0 x P1,P1 x P2,P2 x P3,...,P5 x P6 6 int m[NUM][NUM]; //最少乘次數 / 最優值數組 7 int s[NUM][NUM]; //最優斷開位置 8 9 int LookupChain(int i, int j) 10 { 11 if(m[i][j] > 0) return m[i][j]; 12 if(i == j) return 0; 13 int u = LookupChain(i, i) + LookupChain(i+1, j) + p[i-1]*p[i]*p[j]; 14 s[i][j] = i; 15 for(int k = i+1; k < j; k++) 16 { 17 int t = LookupChain(i, k) + LookupChain(k+1, j) + p[i-1]*p[k]*p[j]; 18 if(t < u) {u = t; s[i][j] = k;} 19 } 20 m[i][j] = u; 21 return u; 22 } 23 24 int MemoizedMatrixChain(int n) 25 { 26 for(int i = 1; i <= n; i++) 27 for(int j = i; j <= n; j++) m[i][j] = 0; 28 return LookupChain(1, n); 29 30 } 31 32 void TraceBack(int i, int j) 33 { 34 if(i==j) 35 cout<< "A" << i; 36 else 37 { 38 cout << "("; 39 TraceBack(i, s[i][j]); 40 TraceBack(s[i][j]+1, j); 41 cout << ")"; 42 } 43 } 44 45 int main() 46 { 47 int n; 48 cin >> n; //矩陣的個數 49 int temp; 50 for(int i = 0; i < n; i++) 51 cin >> p[i] >> temp; //矩陣的維數 52 p[n] = temp; 53 MemoizedMatrixChain(n); 54 cout << m[1][n] << endl; //最少乘次數 55 TraceBack(1, n); //按照最優斷開位置列出乘法順序 56 return 0; 57 }
動態規劃與備忘錄方法比較:
(1)相同點
這兩種算法都利用了子問題重疊性質。對每一個子問題,兩種方法都只解一次,並記錄答案。再次遇到該子問題時,不從新求解而簡單地取用已獲得的答案,節省了計算量,提升了算法的效率。
(2)不一樣點
動態規劃是自底向上的方式計算;備忘錄是自頂向下的方式計算。
當一個問題的全部子問題都至少要解一次時,用動態規劃算法比用備忘錄方法好;當子問題中的部分子問題可沒必要求解時,用備忘錄方法則較有利,由於從其控制結構能夠看出,該方法只解那些須要求解的子問題。
三、最長公共子序列
最長公共子序列的結構:
設序列 X = {x1,x2,…,xm} 和 Y = {y1,y2,…,yn} 的最長公共子序列爲 Z = {z1,z2,…,zk},則
a. 若 xm = yn,則 zk = xm = yn,且 zk-1 是 xm-1 和 yn-1 的最長公共子序列;
b. 若 xm ≠ yn 且 zk ≠ xm,則 Z 是 xm-1 和 Y 的最長公共子序列;
c. 若 xm ≠ yn 且 zk ≠ yn,則 Z 是 X 和 yn-1 的最長公共子序列。
總結:(1)兩個序列的最長公共子序列包含了這兩個序列的前綴的最長公共子序列;
(2)最長公共子序列問題具備最優子結構性質。
子問題的遞歸結構:
由最長公共子序列問題的最優子結構性質可知,要找出 X 和 Y 的最長公共子序列,可按如下方式遞歸地進行:
a. 當 xm = yn 時,找出 Xm-1 和 Yn-1 的最長公共子序列,而後在其尾部加上 xm(=yn)便可得 X 和 Y 的一個最長公共子序列;
b. 當 xm ≠ yn 時,必須解兩個子問題,即找出 Xm-1 和 Y 的一個最長公共子序列及 X 和 Yn-1 的一個最長公共子序列。
這兩個公共子序列中較長者爲 X 和 Y 的一個最長公共子序列。
用 c[i][j] 記錄序列 Xi 和 Yj 的最長公共子序列的長度。Xi = {x1,x2,…,xi},Yj = {y1,y2,…,yj}。
當 i = 0 或 j = 0 時,空序列是 Xi 和 Yj 的最長公共子序列,故此時 C[i][j] = 0。其它狀況下,由最優子結構性質可創建遞歸關係以下:
圖3.1 由最優子結構性質創建遞歸關係
例如:X = {A,B,C,B,D,A,B},Y = {B,D,C,A,B,A}。
最長公共子序列爲 4,即{B,C,B,A}。
1 #include<iostream> 2 using namespace std; 3 4 #define NUM 100 5 int c[NUM][NUM]; //最長公共子序列中的字母個數 6 int b[NUM][NUM]; //存放方向編號 7 8 void LCSLength(int m, int n, char x[],char y[]) 9 { //數組c的第0行、第0列置0 10 for (int i = 1; i <= m; i++) c[i][0] = 0; 11 for (int i = 1; i <= n; i++) c[0][i] = 0; 12 13 //根據遞推公式構造數組c 14 for(int i = 1; i <= m; i++) 15 for(int j = 1; j <= n; j++) 16 { 17 if(x[i] == y[j]) 18 {c[i][j] = c[i-1][j-1] + 1; b[i][j] = 1;} // ↖ 19 else if(c[i-1][j] >= c[i][j-1]) 20 {c[i][j] = c[i-1][j]; b[i][j] = 2;} // ↑ 21 else 22 {c[i][j] = c[i][j-1]; b[i][j] = 3;} // ← 23 } 24 } 25 26 void LCS(int i, int j, char x[]) 27 { 28 if(i == 0 || j == 0) return; 29 if(b[i][j] == 1) {LCS(i-1, j-1, x); cout << x[i];} 30 else if(b[i][j] == 2) LCS(i-1, j, x); 31 else LCS(i, j-1, x); 32 } 33 34 int main() 35 { 36 char x[NUM]; 37 char y[NUM]; 38 int m, n; 39 cin >> m; 40 for(int i = 1; i <= m; i++) 41 cin >> x[i]; 42 cin >> n; 43 for(int i = 1; i <= n; i++) 44 cin >> y[i]; 45 LCSLength(m, n, x, y); 46 cout << c[m][n] << endl; 47 LCS(m, n, x); 48 return 0; 49 }
四、最大子段和
給定由 n 個整數(包含負整數)組成的序列 a1,a2,...,an,求該序列子段和的最大值。
當全部整數均爲負值時定義其最大子段和爲 0。所求的最優值爲:
圖4.1 最大子段和公式
例如:
當(a1,a2, ……a7,a8)= (1,-3,7,8,-4,12, -10,6)時,最大子段和爲:
圖4.2 最大子段和實例
(1)最大子段和分治算法
所給的序列 a[1:n] 分爲長度相等的兩段 a[1:n/2] 和 a[n/2+1:n] ,分別求出這兩段的最大子段和,則 a[1:n] 的最大子段和有三種情形:
Ⅰ. a[1:n] 的最大子段和與 a[1:n/2] 的最大子段和相同;
Ⅱ. a[1:n] 的最大子段和與 a[n/2+1:n] 的最大子段和相同;
Ⅲ. a[1:n] 的最大子段和爲 ai+…+aj,且 1 ≤ i ≤ n/2,n/2+1 ≤ j ≤ n。
1 #include<iostream> 2 using namespace std; 3 4 #define NUM 50 5 6 int MaxSubSum(int *a, int left, int right) 7 { 8 int sum = 0; 9 if(left == right) sum = a[left] > 0 ? a[left] : 0; 10 else 11 { 12 int center = (left + right) / 2; 13 int leftsum = MaxSubSum(a, left, center); 14 int rightsum = MaxSubSum(a, center+1, right); 15 16 int s1 = 0; 17 int lefts = 0; 18 for(int i = center; i >= left; i--) 19 { 20 lefts += a[i]; 21 if(lefts > s1) 22 s1 = lefts; 23 } 24 25 int s2 = 0; 26 int rights = 0; 27 for(int i = center+1; i <= right; i++) 28 { 29 rights += a[i]; 30 if(rights > s2) 31 s2 = rights; 32 } 33 34 sum = s1 + s2; 35 if(sum < leftsum) sum = leftsum; 36 if(sum < rightsum) sum = rightsum; 37 } 38 return sum; 39 } 40 41 int MaxSum(int n, int *a) 42 { 43 return MaxSubSum(a, 1, n); 44 } 45 46 int main() 47 { 48 int a[NUM] = {0}; 49 int n; 50 cin >> n; 51 for(int i = 1; i <= n; i++) 52 cin >> a[i]; 53 cout<< MaxSum(n, a) <<endl; 54 return 0; 55 }
(2)最大子段和動態規劃算法
由bj的定義易知,當 bj-1 > 0 時 bj = bj-1 + aj,不然 bj = aj。則計算 bj 的動態規劃遞歸式:bj = max{bj-1+aj, aj},1 ≤ j ≤ n。
1 #include<iostream> 2 using namespace std; 3 4 #define NUM 50 5 6 int MaxSum(int n, int *a) 7 { 8 int sum = 0, b = 0; 9 for(int i = 1; i <= n; i++) 10 { 11 if(b > 0) b += a[i]; 12 else b = a[i]; 13 if(b > sum) sum = b; 14 } 15 return sum; 16 } 17 18 int main() 19 { 20 int a[NUM] = {0}; 21 int n; 22 cin >> n; 23 for(int i = 1; i <= n; i++) 24 cin >> a[i]; 25 cout<< MaxSum(n, a) <<endl; 26 return 0; 27 }
五、最長單調遞增子序列
例如:
a[] = {1,6,2,4,3}。
則最長單調遞增子序列爲 3,即 {1,2,3}。
1 #include<iostream> 2 using namespace std; 3 4 #define NUM 50 5 6 int LIS(int n, int *a) 7 { 8 int b[NUM] = {0}; //每一步最長單調遞增子序列 9 b[0] = 1; 10 int max = 0; 11 for(int i = 1; i < n; i++) 12 { 13 int k = 0; 14 for(int j = 0; j < i; j++) { 15 if(a[j] <= a[i] && k < b[j]) { 16 k = b[j]; 17 } 18 b[i] = k+1; 19 } 20 21 if(max < b[i]) max = b[i]; 22 } 23 return max; 24 } 25 26 int main() 27 { 28 int a[NUM]; 29 int n; 30 cin >> n; 31 for(int i = 0; i < n; i++) 32 cin >> a[i]; 33 int ans = LIS(n, a); 34 cout << ans <<endl; 35 return 0; 36 }
六、0-1揹包
給定一個物品集合 s ={1,2,3,…,n},物品i的重量是 wi,其價值是 vi,揹包的容量爲 W,即最大載重量不超過 W。在限定的總重量 W 內,咱們如何選擇物品,才能使得物品的總價值最大。
注意:(1)若是物品不能被分割,即物品i要麼整個地選取,要麼不選取;
(2)不能將物品 i 裝入揹包屢次,也不能只裝入部分物品 i,則該問題稱爲 0—1 揹包問題;
(3)若是物品能夠拆分,則問題稱爲揹包問題,適合使用貪心算法。
例如:
1 #include <iostream> 2 using namespace std; 3 4 #define NUM 50 5 6 void Knapsack(int v[], int w[], int c, int n, int m[][10]) 7 { 8 int jMax = min(w[n]-1,c); //揹包剩餘容量上限,範圍[0..w[n]-1] 9 for(int j = 0; j <= jMax; j++) m[n][j] = 0; 10 for(int j = w[n]; j <= c; j++) m[n][j] = v[n]; //限制範圍[w[n]~c] 11 12 for(int i = n-1; i > 1; i--) 13 { 14 jMax = min(w[i]-1, c); 15 for(int j = 0; j <= jMax; j++)//揹包不一樣剩餘容量 j <= jMax < c 16 m[i][j] = m[i+1][j];//沒產生任何效益 17 18 for(int j = w[i]; j <= c; j++) //揹包不一樣剩餘容量 j-wi > c 19 m[i][j] = max(m[i+1][j], m[i+1][j-w[i]]+v[i]); //價值增長vi 20 } 21 m[1][c] = m[2][c]; 22 if(c >= w[1]) m[1][c] = max(m[1][c], m[2][c-w[1]]+v[1]); 23 } 24 25 void Traceback(int m[][10], int w[], int c, int n, int x[]) 26 { 27 for(int i=1; i<n; i++) 28 if(m[i][c] == m[i+1][c]) x[i]=0; 29 else {x[i]=1; c-=w[i];} 30 x[n] = (m[n][c]) ? 1 : 0 ; 31 } 32 33 int main() 34 { 35 int c = 5; 36 int w[] = {0,2,1,3,2}; 37 int v[] = {0,12,10,20,15}; 38 int x[NUM]; //存儲被選中的物品編號,選中x[i] = 1,不然x[i] = 0 39 int m[10][10]; 40 int n = (sizeof(v) / sizeof(v[0])) - 1; 41 42 Knapsack(v, w, c, n, m); 43 44 cout << "揹包能裝的最大價值爲:" << m[1][c] <<endl; 45 46 Traceback(m, w, c, n, x); 47 48 cout<<"揹包裝下的物品編號爲:"; 49 for(int i = 1; i <= n; i++) 50 { 51 if(x[i] == 1) cout<<i<<" "; 52 } 53 cout<<endl; 54 return 0; 55 }