動態規劃分析
動態規劃
通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。動態規劃常常適用於有重疊子問題和最優子結構性質的問題。
基本思想
若要解一個給定問題,我們需要解其不同部分(即子問題),再合併子問題的解以得出原問題的解。 通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量: 一旦某個給定子問題的解已經算出,則將其記憶化儲存,以便下次需要同一個子問題解之時直接查表。 這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。
分治與動態規劃
共同點:二者都要求原問題具有最優子結構性質,都是將原問題分而治之,分解成若干個規模較小(小到很容易解決的程式)的子問題.然後將子問題的解合併,形成原問題的解.
不同點:分治法將分解後的子問題看成相互獨立的,通過用遞迴來做。
動態規劃將分解後的子問題理解為相互間有聯絡,有重疊部分,需要記憶,通常用迭代來做。
問題特徵
最優子結構:當問題的最優解包含了其子問題的最優解時,稱該問題具有最優子結構性質。
重疊子問題:在用遞迴演算法自頂向下解問題時,每次產生的子問題並不總是新問題,有些子問題被反覆計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只解一次,而後將其解儲存在一個表格中,在以後儘可能多地利用這些子問題的解。
步驟
描述最優解的結構
遞迴定義最優解的值
按自底向上的方式計算最優解的值
由計算出的結果構造一個最優解
注意需要需要二維陣列用容器,C++動態分配二維陣列太坑爹
典型問題
01揹包問題
揹包九講:http://www.cnblogs.com/jbelial/articles/2116074.html
有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
將前i件物品放入容量為v的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化為“前i-1件物品放入容量為v的揹包中”;如果放第i件物品,那麼問題就轉化為“前i-1件物品放入剩下的容量為v-c[i]的揹包中”,此時能獲得的最大價值就是f [i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。
int main() { //int m = 120; //int n = 5; //vector<int> w = { 0, 40, 50, 70, 40, 20 }; //vector<int> v = { 0, 10, 25, 40, 20, 10 }; int m, n; //m重量,n數量 while (cin >> m >> n) { vector<int> w(n + 1, 0); vector<int> v(n + 1, 0); for (int i = 1; i <= n; i++) { int tmp; cin >> tmp; w[i] = tmp; } for (int i = 1; i <= n; i++) { int tmp; cin >> tmp; v[i] = tmp; } vector< vector<int> > vec(n + 1, vector<int>(m + 1, 0)); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (w[i] > j) vec[i][j] = vec[i - 1][j]; else { int tmp1 = v[i] + vec[i - 1][j - w[i]]; int tmp2 = vec[i - 1][j]; vec[i][j] = tmp1 > tmp2 ? tmp1 : tmp2; } } } double val = vec[n][m] * 0.1; cout << val << endl; } system("pause"); }
最長公共子序列(不連續) LCS Longest Common Subsequence
找兩個字串的最長公共子串,這個子串要求在原字串中是連續的。而最長公共子序列則並不要求連續。
cnblogs與belong,最長公共子序列為blog(cnblogs, belong),最長公共子串為lo(cnblogs, belong)
這兩個問題都是用空間換空間,建立一個二維陣列來記錄之前的每個狀態
狀態轉移方程:
用i,j遍歷兩個子串x,y,如果兩個元素相等就+1 ,不等就用上一個狀態最大的元素
1 public static int lcs(String str1, String str2) { 2 int len1 = str1.length(); 3 int len2 = str2.length(); 4 int c[][] = new int[len1+1][len2+1]; 5 for (int i = 0; i <= len1; i++) { 6 for( int j = 0; j <= len2; j++) { 7 if(i == 0 || j == 0) { 8 c[i][j] = 0; 9 } else if (str1.charAt(i-1) == str2.charAt(j-1)) { 10 c[i][j] = c[i-1][j-1] + 1; 11 } else { 12 c[i][j] = max(c[i - 1][j], c[i][j - 1]); 13 } 14 } 15 } 16 return c[len1][len2]; 17 }
最長公共子串(連續)
狀態轉移方程:
區別就是因為是連續的,如果兩個元素不等,那麼就要=0了而不能用之前一個狀態的最大元素
1 public static int lcs(String str1, String str2) { 2 int len1 = str1.length(); 3 int len2 = str2.length(); 4 int result = 0; //記錄最長公共子串長度 5 int c[][] = new int[len1+1][len2+1]; 6 for (int i = 0; i <= len1; i++) { 7 for( int j = 0; j <= len2; j++) { 8 if(i == 0 || j == 0) { 9 c[i][j] = 0; 10 } else if (str1.charAt(i-1) == str2.charAt(j-1)) { 11 c[i][j] = c[i-1][j-1] + 1; 12 result = max(c[i][j], result); 13 } else { 14 c[i][j] = 0; 15 } 16 } 17 } 18 return result; 19 }
KMP
硬幣找零問題
假設有幾種硬幣,如1 5 10 20 50 100,並且數量無限。請找出能夠組成某個數目的找零所使用最少的硬幣數。
解法:
用待找零的數值k描述子結構/狀態,記作sum[k],其值為所需的最小硬幣數。對於不同的硬幣面值coin[0...n],有sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1。對應於給定數目的找零total,需要求解sum[total]的值。
注意要從前往後算,從後往前算無法儲存狀態,需要遞迴,效率很低,就不是動態規劃了
View Code
類似硬幣的問題找平方個數最小
題目:
給一個正整數 n, 找到若干個完全平方數(比如1, 4, 9, ... )使得他們的和等於 n。你需要讓平方數的個數最少。 給出 n = 12, 返回 3 因為 12 = 4 + 4 + 4。 給出 n = 13, 返回 2 因為 13 = 4 + 9。
View Code
最長迴文字串
迴文字串的子串也是迴文,比如P[i,j](表示以i開始以j結束的子串)是迴文字串,那麼P[i+1,j-1]也是迴文字串。這樣最長迴文子串就能分解成一系列子問題了。這樣需要額外的空間O(N^2),演算法複雜度也是O(N^2)。
首先定義狀態方程和轉移方程:
P[i,j]=0表示子串[i,j]不是迴文串。P[i,j]=1表示子串[i,j]是迴文串。
P[i+1][j-1]&&s.at(i)==s.at(j)
初始化是準備兩個元素是迴文的情況aa,bb
1 string findLongestPalindrome(string &s) 2 { 3 const int length=s.size(); 4 int maxlength=0; 5 int start; 6 bool P[50][50]={false}; 7 for(int i=0;i<length;i++)//初始化準備 8 { 9 P[i][i]=true; 10 if(i<length-1&&s.at(i)==s.at(i+1)) 11 { 12 P[i][i+1]=true; 13 start=i; 14 maxlength=2; 15 } 16 } 17 for(int len=3;len<length;len++)//子串長度 18 for(int i=0;i<=length-len;i++)//子串起始地址 19 { 20 int j=i+len-1;//子串結束地址 21 if(P[i+1][j-1]&&s.at(i)==s.at(j)) 22 { 23 P[i][j]=true; 24 maxlength=len; 25 start=i; 26 } 27 } 28 if(maxlength>=2) 29 return s.substr(start,maxlength); 30 return NULL; 31 }
最長遞增序列
問題:設L=<a1,a2,…,an>是n個不同的實數的序列,L的遞增子序列是這樣一個子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
第一種方法,排序,然後用LCS來解決:設序列X=<b1,b2,…,bn>是對序列L=<a1,a2,…,an>按遞增排好序的序列。那麼顯然X與L的最長公共子序列即為L的最長遞增子序列。這樣就把求最長遞增子序列的問題轉化為求最長公共子序列問題LCS了。
第二種:時間複雜度O(N^2)的演算法:
LIS[i]:表示陣列前i個元素中(包括第i個),最長遞增子序列的長度
LIS[i] = max{ LIS[i] , LIS[k]+1 }, 0 <= k < i, a[i]>a[k]
LIS陣列的值表示前i個元素的最長子序列。i從第一個元素到最後一個元素遍歷一遍,j從第一個元素到第i個元素遍歷,如果第i個元素大於j,並且LIS[J] + 1比LIS[I]還大就更新,相當於把j加入到這個遞增序列了
1 int LIS(int a[], int length) 2 { 3 int *LIS = new int[length]; 4 for(int i = 0; i < length; ++i) 5 { 6 LIS[i] = 1; //初始化預設長度 7 for(int j = 0; j < i; ++j) //前面最長的序列 8 if(a[i] > a[j] && LIS[j]+1 > LIS[i]) 9 LIS[i] = LIS[j]+1; 10 } 11 int max_lis = LIS[0]; 12 for(int i = 1; i < length; ++i) 13 if(LIS[i] > max_lis) 14 max_lis = LIS[i]; 15 return max_lis; //取LIS的最大值 16 }
字串相似度/編輯距離(edit distance)
N皇后問題
其他問題
1.某幢大樓有100層。你手裡有兩顆一模一樣的玻璃珠。當你拿著玻璃珠在某一層往下扔的時候,一定會有兩個結果,玻璃珠碎了或者沒碎。這幢大樓有個臨界樓層。低於它的樓層,往下扔玻璃珠,玻璃珠不會碎,等於或高於它的樓層,扔下玻璃珠,玻璃珠一定會碎。玻璃珠碎了就不能再扔。現在讓你設計一種方式,使得在該方式下,最壞的情況扔的次數比其他任何方式最壞的次數都少。也就是設計一種最有效的方式。
例如:有這樣一種方式,第一次選擇在60層扔,若碎了,說明臨界點在60層及以下樓層,這時只有一顆珠子,剩下的只能是從第一層,一層一層往上實驗,最壞的情況,要實驗59次,加上之前的第一次,一共60次。若沒碎,則只要從61層往上試即可,最多隻要試40次,加上之前一共需41次。兩種情況取最多的那種。故這種方式最壞的情況要試60次。仔細分析一下。如果不碎,我還有兩顆珠子,第二顆珠子會從N+1層開始試嗎?很顯然不會,此時大樓還剩100-N層,問題就轉化為100-N的問題了。
那該如何設計方式呢?
根據題意很容易寫出狀態轉移方程:N層樓如果從n層投下玻璃珠,最壞的嘗試次數是:
那麼所有層投下的最壞嘗試次數的最小值即為問題的解:。其中F(1)=1.
1 /* 2 *侯凱,2014-9-15 3 *功能:100樓層拋珠問題 4 */ 5 #include<iostream> 6 using namespace std; 7 8 int max(int a, int b) 9 { 10 return (a > b)? a : b; 11 } 12 13 int dp[101]; 14 //N<=100; 15 int floorThr(int N) 16 { 17 for (int i = 2; i <= N; i++) 18 { 19 dp[i] = i; 20 for (int j = 1; j<i; j++) 21 { 22 int tmp = max(j, 1 + dp[i - j]); //j的遍歷相當於把每層都試一遍 23 if (tmp<dp[i]) 24 dp[i] = tmp; 25 } 26 } 27 return dp[N]; 28 } 29 30 int main() 31 { 32 dp[0] = 0; 33 dp[1] = 1; 34 int dis = floorThr(100); 35 cout << dis << endl; 36 system("Pause"); 37 }
輸出為14,說明在合適的樓層拋玻璃珠,最差情況下只需14次可找到臨界層。
答案是先從14樓開始拋第一次;如果沒碎,再從27樓拋第二次;如果還沒碎,再從39樓拋第三次;如果還沒碎,再從50樓拋第四次;如此,每次間隔的樓層少一層。這樣,任何一次拋棋子碎時,都能確保最多拋14次可以找出臨界樓層。
N*N方格內的走法問題
1 #include<iostream> 2 #include<vector> 3 using namespace std; 4 5 int main() 6 { 7 int n; 8 while (cin >> n) 9 { 10 vector<vector<int>> dp(n+1, vector<int>(n+1, 1)); 11 for (int i = 1; i <= n;i++) 12 { 13 for (int j = 1; j <= n;j++) 14 { 15 dp[i][j] = dp[i][j - 1] + dp[i - 1][j]; 16 } 17 } 18 cout << dp[n][n] << endl; 19 } 20 }
其他問題參考:
http://www.cnblogs.com/wuyuegb2312/p/3281264.html#q1a1
http://www.cnblogs.com/luxiaoxun/archive/2012/11/15/2771605.html