1. 程式人生 > >演算法分析與設計(四)動態規劃(二)

演算法分析與設計(四)動態規劃(二)

動態規劃的概念複習
每次決策依賴於當前狀態,又隨即引起狀態的轉移。一個決策序列就是在變化的狀態中產生的,所以,這種多階段最優化決策解決問題的過程就稱為動態規劃。

動態規劃的思想和策略
將待求解的問題分解為若干個子問題,按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的資訊。
適合用動態規劃求解的問題,經分解後得到的子問題往往不是互相獨立的。(這一點與分治法不同)

能用動態規劃求解的問題一般具有三個性質
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 ;
}