演算法分析與設計(四)動態規劃(二)
動態規劃的概念複習
每次決策依賴於當前狀態,又隨即引起狀態的轉移。一個決策序列就是在變化的狀態中產生的,所以,這種多階段最優化決策解決問題的過程就稱為動態規劃。
動態規劃的思想和策略
將待求解的問題分解為若干個子問題,按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的資訊。
適合用動態規劃求解的問題,經分解後得到的子問題往往不是互相獨立的。(這一點與分治法不同)
能用動態規劃求解的問題一般具有三個性質
1.最優化原理(最優子結構性質)
2.無後效性(當前狀態一旦確定,就不受以後狀態決策的影響)
3.有重疊子問題(子問題不相互獨立,因而當前子問題的解可以為以後子問題的解提供參考)
動態規劃求解的基本步驟
1.劃分階段
2.確定狀態和狀態變數
3.確定決策並寫出狀態轉移方程
4.尋找邊界條件
例題深入
1.字串解碼
問題描述:一個包含字母的訊息被加密後變成了只包含數字的字串,我們現在知道加密的規則:A–>1 ; B–>2 …… Z–>26 ;
現在給定一個已經被加密的只包含數字的字串,求出該字串有多少種被解密的方法。例如 “12” -> AB 或者 “12”->L 。
分析:假設定義一個數組,dp[i]為到第i個數字所能夠組成的所有解碼方式的個數,那麼對於dp[i+1]來說,如果第i個數字和第i+1個數字不能構成一個字元的編碼,那麼第i+1個數字單獨解碼,解碼方式的個數和有i個數字是相同的,即 dp[i+1] = dp[i] ;反之,如果第i個數字和第i+1個數字能構成一個字元的編碼,那麼解碼方式的個數就等於前i-1個數字的解碼方式個數加上前i個數字的解碼方式的個數,即dp[i+1] = dp[i] + dp[i-1],因為此時你可以選擇第i+1個數字單獨解碼,那麼方法數等於dp[i],或者第i個和第i+1個一起編碼,方法數等於dp[i-1]。
程式碼實現
#include<iostream>
#include<string>
#include<vector>
using namespace std;
/**
* 求解字串的解碼方法總數
* @param str 需要解碼的字串
* @return int 解碼方法的總數
*/
int Decod_num(string& str){
//定義一個數組記錄解碼方式的個數
vector<int> vec( str.size() , 1 );
//只有一個數字,解碼方式就一種
if( str.size() < 2 ){
return 1 ;
}
//26以內的數字,解碼方式兩種
if( str[0] == '1' || (str[0] == '2' && str[1] <= '6')){
vec[1] = 2 ;
}
int i ;
int tmp ;
//動態規劃求解過程
for( i = 2 ; i < str.size() ; i ++ ){
//判斷是合法的字元
if( str[i] >= '0' && str[i] <= '9'){
//狀態轉移1,i個數字的解碼方法數至少是前i-1個數字的解碼方法數
vec[i] = vec[i-1];
}else{
return 0 ;
}
tmp = str[i-1] - '0';
tmp = tmp*10 + str[i]-'0';
//判斷最後兩個數字是否能構成一個字元的編碼
if( str[i-1] != '0' && tmp <= 26){
//狀態轉移2, i個數字的解碼方法數等於前i-1個數字的解碼方法數加上前i-2個數字的解碼方法數
vec[i] += vec[i-2];
}
}
//陣列的最後一位即當前字串的解碼方法總數
return vec[str.size()-1];
}
2.矩陣最小路徑和
問題:給定一個二維矩陣,矩陣的每個元素指定了走到該出所需要的代價,要你從矩陣左上角到右下角,尋找代價最小的一條路徑。
分析:到達矩陣的一個點,有兩種走法,一是從上面一個格子走過來,一是從左邊的格子走過來(邊界點除外)。那麼,到達一點的最短路徑,要麼就是到達該點左邊一個點的最小代價加上該點的代價,要麼就是到達該點上面一個點的最小代價加上該點的代價,兩者中的最小值。
即狀態轉移方程
dp[i][j] = min( dp[i-1][j] + vec[i][j] , dp[i][j-1] + vec[i][j] )
程式碼實現
/**
* 求解矩陣從左上角到右下角的最小路徑代價
* @param vec 矩陣的二維陣列
* @return int 最小的路徑代價
*/
int MinPathSum( vector<vector<int>> & vec ){
vector<vector<int>> dp( vec.size() );
int i,j ;
//初始化動態規劃需要的陣列
for( i = 0 ; i < vec.size() ; i ++ ){
dp[i].assign(vec[i].size(),numeric_limits<int>::max());
}
dp[0][0] = vec[0][0];
//初始化邊界值
for( i = 1 ; i < vec.size() ; i++ ){
dp[i][0] = vec[i][0]+dp[i-1][0];
}
for( j = 1 ; j < vec[0].size() ; j++ ){
dp[0][j] = vec[0][j]+dp[0][j-1];
}
//求解過程
int temp ;
for( i = 1 ; i < vec.size() ; i ++ ){
for( j = 1 ; j < vec[0].size() ; j ++ ){
tmp = min(vec[i][j] + dp[i][j-1] , vec[i][j] + dp[i-1][j]);
if( tmp < dp[i][j] ){
dp[i][j] = temp ;
}
}
}
return dp[vec.size()-1][vec[0].size()-1];
}
3.最大子陣列乘積
問題:給定一個整數陣列,求解乘積最大的子陣列的值
分析:由於陣列中可能出現負數,所以當前最大值,可能是之前最大乘以當前值(如果之前最大乘積為正數,且當前數也為正數),也可能是之前最小乘以當前值(如果之前最小乘積為負數,且當前值也為負數,負負得正),也可能是當前數。
所以為了得到全域性最優,我們需要兩個陣列來儲存區域性最優值,一個儲存區域性最大值(正數),一個儲存區域性最小值(負數),並不斷更新兩個區域性最優。
程式碼實現
/**
* 求解最大子陣列乘積
* @param vec 一維陣列
* @return int 最大乘積
*/
int maxProduct( vector<int>& vec){
if( vec.size() == 0 ){
return 0 ;
}
//一維規劃,但是需要兩個陣列來儲存兩個區域性最優值,以得到全域性最大
vector<int> maxcur(vec.size(),0);
vector<int> mincur(vec.size(),0);
maxcur[0] = vec[0];
mincur[0] = vec[0];
int maxproduct = vec[0];
int i , temp ;
for( i = 1 ; i < vec.size() ; i ++ ){
//更新區域性最大值
maxcur[i] = max( vec[i] , max(maxcur[i-1]*vec[i],mincur[i-1]*vec[i]));
//更新區域性最小值
mincur[i] = min( vec[i] , min(mincur[i-1]*vec[i],maxcur[i-1]*vec[i]));
//更新全域性最大值
maxproduct = max( maxcur[i] , maxproduct );
}
return maxproduct ;
}