菜鳥學算法——動態規劃(二)

  • 概述
動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法,它是應用數學中用於解決某類最優化問題的重要工具。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),根據一類多階段問題的特點,把多階段決策問題變換爲一系列互相聯繫的單階段問題,利用各階段之間的關係,逐個加以解決。創立了解決這類過程優化問題的新方法--動態規劃。一些靜態模型,只要人爲地引進“時間”因素,分成時段,就可以轉化成多階段的動態模型,用動態規劃方法去處理。1957年出版了他的名著《Dynamic Programming》,這是該領域的第一本著作。
“一個過程的最優決策具有這樣的性質:即無論其初始狀態和初始決策如何,其今後諸策略對以第一個決策所形成的狀態作爲初始狀態的過程而言,必須構成最優策略”。簡言之,一個最優策略的子策略,對於它的初態和終態而言也必是最優的
作爲一種使用多階段決策過程最優的通用方法,動態規劃過程是:每次決策依賴於當前狀態,又隨即引起狀態的轉移。一個決策序列就是在變化的狀態中產生出來的,所以,這種多階段最優化決策解決問題的過程就稱爲動態規劃。
動態規劃一般可分爲線性動規,區域動規,樹形動規,揹包動規四類。線性動規包括攔截導彈,合唱隊形,挖地雷,建學校,劍客決鬥等;區域動規包括石子合併, 加分二叉樹,統計單詞個數,炮兵佈陣等;樹形動規包括貪喫的九頭龍,二分查找樹,聚會的歡樂,數字三角形等;揹包問題包括0/1揹包問題,完全揹包問題,分組揹包問題,二維揹包,裝箱問題,擠牛奶等。動態規劃問世以來,在經濟管理、生產調度、工程技術和最優控制等方面得到了廣泛的應用。例如最短路線、庫存管理、資源分配、設備更新、排序、裝載等問題,用動態規劃方法比用其它方法求解更爲方便。
  • 基本思想     

基本思想與分治法類似,也是將待求解的問題分解爲若干個子問題(階段),按順序求解子階段,前一子問題的解,爲後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各種可能的局部解,通過決策保留那些有可能達到最優的局部解,丟棄其他局部解。依次解決各子問題,最後一個子問題就是初始問題的解。由於動態規劃解決的問題多數有重疊子問題這個特點,爲減少重複計算,對每一個子問題只解一次,將其不同階段的不同狀態保存在一個二維數組中。
與分治法最大的差別是:適合於用動態規劃法求解的問題,經分解後得到的子問題往往不是互相獨立的(即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解)。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。如果我們能夠保存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重複計算,節省時間。
如果問題是由交疊的子問題所構成,我們就可以用動態規劃技術來解決它,一般來說,這樣的子問題出現在對給定問題求解的遞推關係中,這個遞推關係包含了相
同問題的更小子問題的解。動態規劃法建議,與其對交疊子問題一次又一次的求解,不如把每個較小子問題只求解一次並把結果記錄在表中(動態規劃也是空間換時間
的),這樣就可以從表中得到原始問題的解。
我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。具體的動態規劃算法多種多樣,但它們具有相同的填表格式。
  • 適用條件
能採用動態規劃求解的問題的一般要具有3個性質:

1.最優化原理(最優子結構性質): 如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。最優化原理可這樣闡述:不論過去狀態和決策如何,對前面的決策所形成的狀態而言,餘下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略總是最優的。

2.無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱爲無後效性。

3.子問題的重疊性:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃算法同其他算法相比就不具備優勢)。和分治算法比較類似,但不同的是分治算法把原問題劃歸爲幾個相互獨立的子問題,從而一一解決,而動態規劃則是針對子問題有重疊的情況的一種解決方案。

動態規劃將原來具有指數級時間複雜度的搜索算法改進成了具有多項式時間複雜度的算法。其中的關鍵在於解決冗餘,這是動態規劃算法的根本目的。動態規劃實質上是一種以空間換時間的技術,它在實現的過程中,不得不存儲產生過程中的各種狀態,所以它的空間複雜度要大於其它的算法。

  • 基本步驟
動態規劃所處理的問題是一個多階段決策問題,一般由初始狀態開始,通過對中間階段決策的選擇,達到結束狀態。這些決策形成了一個決策序列,同時確定了完成整個過程的一條活動路線(通常是求最優的活動路線)。一般要經歷以下幾個步驟。
 (1)劃分階段:按照問題的時間或空間特徵,把問題分爲若干個階段。在劃分階段時,注意劃分後的階段一定要是有序的或者是可排序的,否則問題就無法求解。
(2)確定狀態和狀態變量:將問題發展到各個階段時所處於的各種客觀情況用不同的狀態表示出來。當然,狀態的選擇要滿足無後效性。
(3)確定決策並寫出狀態轉移方程:因爲決策和狀態轉移有着天然的聯繫,狀態轉移就是根據上一階段的狀態和決策來導出本階段的狀態。所以如果確定了決策,狀態轉移方程也就可寫出。但事實上常常是反過來做,根據相鄰兩個階段的狀態之間的關係來確定決策方法和狀態轉移方程。
(4)尋找邊界條件:給出的狀態轉移方程是一個遞推式,需要一個遞推的終止條件或邊界條件。
一般,只要解決問題的階段、狀態和狀態轉移決策確定了,就可以寫出狀態轉移方程(包括邊界條件)。
實際應用中可以按以下幾個簡化的步驟進行設計
1)分析最優解的性質,並刻畫其結構特徵。
(2)遞歸的定義最優解
(3)以自底向上或自頂向下的記憶化方式(備忘錄法)計算出最優值

(4)根據計算最優值時得到的信息,構造問題的最優解

 使用動態規劃求解問題,最重要的就是確定動態規劃三要素:

(1)問題的階段 (2)每個階段的狀態  (3)從前一個階段轉化到後一個階段之間的遞推關係。

  遞推關係必須是從次小的問題開始到較大的問題之間的轉化,從這個角度來說,動態規劃往往可以用遞歸程序來實現,不過因爲遞推可以充分利用前面保存的子問題的解來減少重複計算,所以對於大規模問題來說,有遞歸不可比擬的優勢,這也是動態規劃算法的核心之處。

    確定了動態規劃的這三要素,整個求解過程就可以用一個最優決策表來描述,最優決策表是一個二維表,其中行表示決策的階段,列表示問題狀態,表格需要填寫的數據一般對應此問題的在某個階段某個狀態下的最優值(如最短路徑,最長公共子序列,最大價值等),填表的過程就是根據遞推關係,從1行1列開始,以行或者列優先的順序,依次填寫表格,最後根據整個表格的數據通過簡單的取捨或者運算求得問題的最優解。

          f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

一個動態規劃算法的幾個關鍵點
1)怎麼描述問題,要把問題描述爲交疊的子問題
2)交疊子問題的初始條件(邊界條件)
3)動態規劃在形式上往往表現爲填矩陣的形式(在後面會看到,有的可以優化空間複雜度,開一個數組即可,優化也是根據遞推式的依賴形式的,後面有篇文章詳細說明)
4)填矩陣的方式(或者說順序)表明了什麼?--它表明了這個動態規劃從小到大產生的過程,專業點的說就是遞推式的依賴形式決定了填矩陣的順序

個人體會是動態規劃的難點在於前期的設計:
a)怎麼描述問題,使它能表述爲一個動態規劃問題(具備什麼特徵?最有子結構,多階段決策,思考)
b)遞推式的寫出(逆向思維去分析或正向思維去遞歸),確定你要求的是哪個值
c)有了遞推式可以畫個矩陣的圖(一般只從式子上不太容易看出來,當然,對於牛人來說可以藐視),在圖中關注以下兩點:
初始條件
填矩陣的順序(即怎麼去寫代碼控制語句)
有了這些之後,其實動態規劃的代碼都很簡單,它的難點在於問題的描述和解決階段,而不在於寫代碼的階段,剩下的寫代碼基本上就是照着公式填矩陣。

目前design DP主要有兩個思路:

一個是利用recursive method,即首先把問題用遞歸的方法解決,然後用一個table保存recursive中的中間結果,這不就避免了遞歸中重複計算的低效了嗎?遇到需要計算以前計算過的東西,直接查表就OK,總之一句話,先寫recursive,然後比葫蘆畫瓢基本就能把DP的方法寫出來。這裏的難點是如何找到recursive。算法導論裏面也給的是這個思路。下面的前三個例子全部出自《算法導論》。

另一個思路是exhaust search,這個好像是我們老師發明的方法,這裏有篇Kirk的論文, How to design dynamic programming algorithms sans recursion  有興趣的大家可以仔細研究一下,我下面也會簡單舉例介紹一下這個方法。

下面的例子中多數代碼都是僞代碼,旨在illustrate idea。同時節省時間。代碼中都省去了backtrack的過程,即只得到了optimal solution的值,省去了如何construct optimal solution的過程。這個一般用一個數組記錄一下就OK了。

  • 示例

1.計算二項式係數:

參考:

http://www.cnblogs.com/kkgreen/archive/2011/06/26/2090702.html



在排列組合裏面,我們有下面的式子(很容易用組合的定義來證明):



這個式子將C(n , k)的計算問題表述爲了問題描述)C(n-1 , k -1)和C(n -1, k)兩個較小的交疊子問題

初始條件:C(n , n) = C(n , 0) = 1

我們可以用下列填矩陣的方式求出C(n , k):



該算法的時間複雜度是多少呢?可以大概的估計下,只填了下三角矩陣,爲n*k/2  =  n*k,具體的次數爲:

按行來填矩陣:算法僞代碼:



第1個for是控制行的,要填到第n行。第2個for來控制每行填到哪的,到i和k的較小值。從這2個for也可以看出複雜度是n*k。

package Section8;
/*第八章 動態規劃 計算二項式係數*/
publicclass BinoCoeff {

/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
int result = Binomial(8,3);
System.out.println("輸出8的二項式係數:");
for(int i =0;i <=8;i++)
System.out.println("C"+"("+8+","+ i +")"+" ———— "+ Binomial(8,i));
}

publicstaticint Binomial(int n,int k){
//計算二項式係數C(n,k)
int[][] result =newint[n+1][n+1];
for(int i =0;i <= n;i++) //按行來填矩陣
{
for(int j =0;j <= min(i,k);j++) // min(i,k)是這一行需要填的列數
{
//if(j == 0 || j == k)//書上寫錯了
if(j ==0|| j == i)
result[i][j] =1;
else
result[i][j] = result[i -1][j -1] + result[i -1][j];
}
}

return result[n][k];
}

privatestaticint min(int i,int k){
if(i < k)
return i;
return k;
}

}

如何優化空間複雜度
——爲什麼可以優化,上面說過,可不可以優化,以及如何優化空間複雜度依賴於它的遞推形式:
——從填矩陣的那張圖可以看出,這個動態規劃產生各項的過程(如果按行填的話)是上一行的第 i-1 項和第 i 項加起來產生下一行的第 i 項,傳統上,我們從左往右填。
——事實上,根據它的產生過程(這個產生過程依賴於遞推式自身的數學特徵),可以從右往左填,這樣開一個數組就行,在原數組上本地不動的填數,從右往左填可以保證一個位置在覆蓋以後不會再被用到(這是由遞推式的屬性決定的,需要畫一畫纔看的比較清楚)。這樣開一個K大的數組就行了,具體的實現就不寫了,已經分析的很清楚了,實現也不難


2.國際象棋最短路徑問題

參考:

http://www.cnblogs.com/kkgreen/archive/2011/06/26/2090702.html

國際象棋中的車可以水平的或豎直的移動,一個車要從一個棋盤的一角移到對角線的另一角,有多少種最短路徑?
a,用動態規劃算法求解
b,用初等排列組合知識求解

a)主要是培養下怎麼去建立動態規劃的遞推式

問題是從(0,0)移動到(n,n)有多少種方法?(最短路,即橫n豎n,不能回退)

設C[i , j]表示從(0,0)移動到(i ,j)的方法數(描述問題,怎麼去刻畫C[i , j]的含義,是動態規劃的一個關鍵點):

那麼怎麼才能走到(i ,j)呢,它的上一步必定是(i-1 ,j)或者(i ,j-1)-------(分析動態規劃問題的逆向思維,很重要,後面要講)這樣就將問題描述爲了交疊子問題:

C[i , j]  =  C[i -1, j]   +  C[i , j-1]        ( C[i , j]的含義  )

我們要求的是C[n , n]

初始條件:

C[0 , j]  =  j  j從0到n  

C[i , 0]   =  i i從0到n

即第一行第一列確定。

填矩陣的形式:可以按行也可以按列。以上分析畫個圖很容易看出來。剩下的實現就很簡單了。

b)這是個很簡單的高中排列組合題目了,假設棋盤大小是n*n的(囧,象棋棋盤多大這個得想想才知道,就說n吧),答案是C(2n , n)

3.計算比賽勝率問題
菜鳥學算法——動態規劃(二) - IMAX - IMAX 的博客
 第一問就是個概率題,聽起來比較拗口,其實不難,屬於高中概率水平:

有了遞推式後,發現其實跟上一題完全一樣,就是遞推式裏多乘了個概率值。

我們要求的是P[i , j]

初始條件:  

C[0 , j]  =  q^j  j從0到n  

C[i , 0]   =  p^i i從0到n       

即第一行第一列確定。

填矩陣的形式:可以按行也可以按列。以上分析畫個圖很容易看出來。剩下的實現就很簡單了。

4.0/1揹包問題

01揹包是在N件物品取出若干件放在空間爲C的揹包裏,每件物品的重量爲W1,W2,…,Wn,與之相對應的價值爲V1,V2,…,Vn.求解將那些物品裝入揹包可使總價值最大。

01揹包: 有N件物品和一個重量爲M的揹包。(每種物品均只有一件)第i件物品的重量是w[i],價值是p[i]。求解將哪些物品裝入揹包可使價值總和最大。

完全揹包: 有N種物品和一個重量爲M的揹包,每種物品都有無限件可用。第i種物品的重量是w[i],價值是p[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包重量,且價值總和最大。

多重揹包: 有N種物品和一個重量爲M的揹包。第i種物品最多有n[i]件可用,每件重量是w[i],價值是p[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包重量,且價值總和最大。

 動態規劃(DP)求解:

        1) 子問題定義:F[i][j]表示前i件物品中選取若干件物品放入剩餘空間爲j的揹包中所能得到的最大價值。

        2) 根據第i件物品放或不放進行決策

                                                  (1-1)

        其中F[i-1][j]表示前i-1件物品中選取若干件物品放入剩餘空間爲j的揹包中所能得到的最大價值;

        而F[i-1][j-C[i]]+W[i]表示前i-1件物品中選取若干件物品放入剩餘空間爲j-C[i]的揹包中所能取得的最大價值加上第i件物品的價值。

        根據第i件物品放或是不放確定遍歷到第i件物品時的狀態F[i][j]。

        設物品件數爲N,揹包容量爲V,第i件物品體積爲C[i],第i件物品價值爲W[i]。

#include<iostream>
 
#define max(a,b) ((a) > (b) ? a : b)
int c[5] = {3,5,2,7,4};
int v[5] = {2,4,1,6,5};
int f[6][10] = {0};
//f[i][v] = max{ f[i-1][v] , f[i-1][v - c[i]] + w[i]}
 
int main()
{
    for(int i = 1; i < 6; i++)
        for(int j = 1; j < 10 ;j++)
        {
            if(c[i] > j)//如果揹包的容量,放不下c[i],則不選c[i]
                f[i][j] = f[i-1][j];       
            else
            {
                f[i][j] = max(f[i-1][j], f[i-1][j - c[i]] + v[i]);//轉移方程式
            }
        }
        std::cout<<f[5][9];
    return 0;
}


public class Pack01 {
public int [][] pack(int m,int n,int w[],int p[]){
//c[i][v]表示前i件物品恰放入一個重量爲m的揹包可以獲得的最大價值
int c[][]= new int[n+1][m+1];
for(int i = 0;i<n+1;i++)
c[i][0]=0;
for(int j = 0;j<m+1;j++)
c[0][j]=0;
//
for(int i = 1;i<n+1;i++){
for(int j = 1;j<m+1;j++){
//當物品爲i件重量爲j時,如果第i件的重量(w[i-1])小於重量j時,c[i][j]爲下列兩種情況之一:
//(1)物品i不放入揹包中,所以c[i][j]爲c[i-1][j]的值
//(2)物品i放入揹包中,則揹包剩餘重量爲j-w[i-1],所以c[i][j]爲c[i-1][j-w[i-1]]的值加上當前物品i的價值
if(w[i-1]<=j){
if(c[i-1][j]<(c[i-1][j-w[i-1]]+p[i-1]))
c[i][j] = c[i-1][j-w[i-1]]+p[i-1];
else
c[i][j] = c[i-1][j];
}else
c[i][j] = c[i-1][j];
}
}
return c;
}
    /**
     * 逆推法求出最優解
     * @param c
     * @param w
     * @param m
     * @param n
     * @return
     */
    public int[] printPack(int c[][],int w[],int m,int n){
        
        int x[] = new int[n];
        //從最後一個狀態記錄c[n][m]開始逆推
        for(int i = n;i>0;i--){
            //如果c[i][m]大於c[i-1][m],說明c[i][m]這個最優值中包含了w[i-1](注意這裏是i-1,因爲c數組長度是n+1)
            if(c[i][m]>c[i-1][m]){
                x[i-1] = 1;
                m-=w[i-1];
            }
        }
        for(int j = 0;j<n;j++)
            System.out.println(x[j]);
        return x;
    }
public static void main(String args[]){
int m = 10;
int n = 3;
int w[]={3,4,5};
int p[]={4,5,6};
Pack01 pack = new Pack01();
int c[][] = pack.pack(m, n, w, p);
pack.printPack(c, w, m,n);
}
}

接下來考慮如何壓縮空間,以降低空間複雜度。

時間複雜度爲O(VN),空間複雜度將爲O(V)

         觀察僞代碼可也發現,F[i][j]只與F[i-1][j]和F[i-1][j-C[i]]有關,即只和i-1時刻狀態有關,所以我們只需要用一維數組F[]來保存i-1時的狀態F[]。假設i-1時刻的F[]爲{a0,a1,a2,…,av},難麼i時刻的F[]中第k個應該爲max(ak,ak-C[i]+W[i])即max(F[k],F[k-C[i]]+W[i]),這就需要我們遍歷V時逆序遍歷,這樣才能保證求i時刻F[k]時F[k-C[i]]是i-1時刻的值。如果正序遍歷則當求F[k]時其前面的F[0],F[1],…,F[K-1]都已經改變過,裏面存的都不是i-1時刻的值,這樣求F[k]時利用F[K-C[i]]必定是錯的值。最後F[V]即爲最大價值。

求F[j]的狀態方程如下:

                                           (1-2)

代碼如下:

#include <iostream>
#include <cstring>
#include "CreateArray.h" //該頭文件用於動態創建及銷燬二維數組,讀者自己實現
using namespace std;

//時間複雜度O(VN),空間複雜度爲O(VN)

int Package01(int Weight[], int Value[], int nLen, int nCapacity)
{
int** Table = NULL;
int** Path = NULL;
CreateTwoDimArray(Table,nLen+1,nCapacity+1); //創建二維數組
CreateTwoDimArray(Path,nLen+1,nCapacity+1); //創建二維數組

for(int i = 1; i <= nLen; i++)
{
for(int j = 1; j <= nCapacity; j++)
{
Table[i][j] = Table[i-1][j];
Path[i][j] = 0;
if(j >= Weight[i-1] && Table[i][j] < Table[i-1][j-Weight[i-1]]+Value[i-1])
{
Table[i][j] = Table[i-1][j-Weight[i-1]]+Value[i-1];
Path[i][j] = 1;
}
}
}

int i = nLen, j = nCapacity;
while(i > 0 && j > 0)
{
if(Path[i][j] == 1)
{
cout << Weight[i-1] << " ";
j -= Weight[i-1];
}
i--;
}
cout << endl;

int nRet = Table[nLen][nCapacity];
DestroyTwoDimArray(Table,nLen+1); //銷燬二維數組
DestroyTwoDimArray(Path,nLen+1); //銷燬二維數組
return nRet;
}

//時間複雜度O(VN),不考慮路徑空間複雜度爲O(V),考慮路徑空間複雜度爲O(VN)

int Package01_Compress(int Weight[], int Value[], int nLen, int nCapacity)
{
int * Table = new int [nCapacity+1];
memset(Table,0,(nCapacity+1)*sizeof(int));
int** Path = 0;
CreateTwoDimArray(Path,nLen+1,nCapacity+1); //創建二維數組

for(int i = 0; i < nLen; i++)
{
for(int j = nCapacity; j >= Weight[i]; j--)
{
Path[i+1][j] = 0;
if(Table[j] < Table[j-Weight[i]]+Value[i])
{
Table[j] = Table[j-Weight[i]]+Value[i];
Path[i+1][j] = 1;
}
}
}

int i = nLen, j = nCapacity;
while(i > 0 && j > 0)
{
if(Path[i][j] == 1)
{
cout << Weight[i-1] << " ";
j -= Weight[i-1];
}

i--;
}
cout << endl;

int nRet = Table[nCapacity];
DestroyTwoDimArray(Path,nLen+1); //銷燬二維數組
delete [] Table;
return nRet;
}

int main()
{
int Weight[] = {2,3,1,4,6,5};
int Value[] = {5,6,5,1,19,7};
int nCapacity = 10;
cout << Package01(Weight,Value,sizeof(Weight)/sizeof(int),nCapacity) << endl;
cout << Package01_Compress(Weight,Value,sizeof(Weight)/sizeof(int),nCapacity) << endl;
return 0;
}

以下是0/1揹包問題的一個例子。

//題目:
//求解下面的揹包問題。有5個體積是3,5,7,8和9,價值爲4,6,7,9和10的物品,揹包的容量是22。
#include<iostream>
using namespace std;
#define N 5 //物體的個數
#define C 22 //揹包的容量
int main(){
//初始化每個物體的體積以及價值,注意數組第一個元素是0
int s[C+1]={0,3,5,7,8,9};
int vv[N+1]={0,4,6,7,9};
//新建一個二維表用於存儲每一步的結果
int v[N+1][C+1];

//初始化二維表
for(int i=0;i<=N;i++){v[i][0]=0;}
for(int j=0;j<=C;j++){v[0][j]=0;}
for(int i=1;i<=N;i++){
for(int j=1;j<=C;j++){
v[i][j]=v[i-1][j];
if(s[i]<=j){
v[i][j]=max(v[i][j],v[i-1][j-s[i]]+vv[i]);
}
}
}
for(int k=0;k<=N;k++){
for(int l=0;l<=C;l++){
cout<<v[k][l];
if(v[k][l]>9)
cout<<' ';
else
cout<<" ";
}
cout<<endl;
}
cout<<"由上表可知,揹包內可裝下的物品的最大價值是:"<<v[N][C]<<endl;system("pause");
return 0;
}

程序運行結果如下圖所示:


5.Rod-cutting problem(切木頭問題)

Input:有一個長n米的木頭,和一個price table,table如下:

長度 i     1  2  3  4  5  6 。。。

價格 Pi 1  5  8  9  10  17。。。   

意思很明顯,就是長度爲1米的木頭可以買1元,長5米的可以賣10元,依次類推

Output:找一個cut的方法,使最後賺的錢最多。

很顯然,這個遞歸的主要思路是我切一刀之後,分成兩段,一段我按table的價錢賣了,另一段我當成一個新的子問題,繼續作爲我的函數的新的參數,這樣不就遞歸了嗎?(*^__^*) 但是問題是這一刀怎麼切,沒錯,我們就來個找最大值,即

r[i][j]=max(r[i-1][j], P[i] + r[i-1][j-i])或

r[j]=max(r[j], P[i] + r[j-i)

根據這個recursive寫DP

//初始化長度以及價值,注意數組第一個元素是0
int p[C+1]={0,1,5,8,9,10};
//新建一個二維表用於存儲每一步的結果
int r[C+1][N+1];

//初始化二維表

for(int i=0;i<=C;i++){r[i][0]=0;}
for(int j=0;j<=N;j++){r[0][j]=0;}
for(int i=1;i<=C;i++){
for(int j=1;j<=N;j++){
r[i][j]=r[i-1][j];
if(i<=j){
r[i][j]=max(r[i-1][j],r[i-1][j-i]+p[i]);
}
}
}

6.編輯距離算法

參考:http://www.cnblogs.com/sking7/archive/2011/10/16/2214044.html

編輯距離概念描述:

編輯距離,又稱Levenshtein距離,是指兩個字串之間,由一個轉成另一個所需的最少編輯操作次數。許可的編輯操作包括將一個字符替換成另一個字符,插入一個字符,刪除一個字符。

例如將kitten一字轉成sitting:

  1. sitten (k→s)
  2. sittin (e→i)
  3. sitting (n→g)

俄羅斯科學家Vladimir Levenshtein在1965年提出這個概念。

 

問題:找出字符串的編輯距離,即把一個字符串s1最少經過多少步操作變成編程字符串s2,操作有三種,添加一個字符,刪除一個字符,修改一個字符

 

解析:

首先定義這樣一個函數——edit(i, j),它表示第一個字符串的長度爲i的子串到第二個字符串的長度爲j的子串的編輯距離。

顯然可以有如下動態規劃公式:

  • if i == 0 且 j == 0,edit(i, j) = 0
  • if i == 0 且 j > 0,edit(i, j) = j
  • if i > 0 且j == 0,edit(i, j) = i
  • if i ≥ 1  且 j ≥ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) },當第一個字符串的第i個字符不等於第二個字符串的第j個字符時,f(i, j) = 1;否則,f(i, j) = 0。

 

 

 0failing
0        
s        
a        
i        
l        
n        

 

 

 0failing
001234567
s1       
a2       
i3       
l4       
n5       

 計算edit(1, 1),edit(0, 1) + 1 == 2,edit(1, 0) + 1 == 2,edit(0, 0) + f(1, 1) == 0 + 1 == 1,min(edit(0, 1),edit(1, 0),edit(0, 0) + f(1, 1))==1,因此edit(1, 1) == 1。 依次類推:

 0failing
001234567
s11234567
a22      
i3       
l4       
n5       

edit(2, 1) + 1 == 3,edit(1, 2) + 1 == 3,edit(1, 1) + f(2, 2) == 1 + 0 == 1,其中s1[2] == 'a' 而 s2[1] == 'f'‘,兩者不相同,所以交換相鄰字符的操作不計入比較最小數中計算。以此計算,得出最後矩陣爲:

 0failing
001234567
s11234567
a22123456
i33212345
l44321234
n55432223

 

程序(C++):注意二維數組動態分配和釋放的方法!!

#include <iostream>
#include <string>

using namespace std;

int min(int a, int b)
{
return a < b ? a : b;
}

int edit(string str1, string str2)
{
int max1 = str1.size();
int max2 = str2.size();

int **ptr = new int*[max1 + 1];
for(int i = 0; i < max1 + 1 ;i++)
{
ptr[i] = new int[max2 + 1];
}

for(int i = 0 ;i < max1 + 1 ;i++)
{
ptr[i][0] = i;
}

for(int i = 0 ;i < max2 + 1;i++)
{
ptr[0][i] = i;
}

for(int i = 1 ;i < max1 + 1 ;i++)
{
for(int j = 1 ;j< max2 + 1; j++)
{
int d;
int temp = min(ptr[i-1][j] + 1, ptr[i][j-1] + 1);
if(str1[i-1] == str2[j-1])
{
d = 0 ;
}
else
{
d = 1 ;
}
ptr[i][j] = min(temp, ptr[i-1][j-1] + d);
}
}

cout << "**************************" << endl;
for(int i = 0 ;i < max1 + 1 ;i++)
{
for(int j = 0; j< max2 + 1; j++)
{
cout << ptr[i][j] << " " ;
}
cout << endl;
}
cout << "**************************" << endl;
int dis = ptr[max1][max2];

for(int i = 0; i < max1 + 1; i++)
{
delete[] ptr[i];
ptr[i] = NULL;
}

delete[] ptr;
ptr = NULL;

return dis;
}

int main(void)
{
string str1 = "sailn";
string str2 = "failing";

int r = edit(str1, str2);
cout << "the dis is : " << r << endl;

return 0;
}

執行效果:

 

7.最大化投資回報問題

某人有一定的資金用來購買不同面額的債卷,不同面額債卷的年收益是不同的,求給定資金,年限以及債卷面額、收益的情況下怎樣購買才能使此人獲得最大投資回報。
程序輸入約定:第一行第一列表示資金(1000的倍數)總量,第二列表示投資年限;第二行表示債卷面額總數;從第三行開始每行表示一種債卷,佔用兩列,前一列表示債卷面額,後一列表示其年收益,如下輸入實例,

10000 1
2
4000 400
3000 250

對於本程序,以示例輸入爲例,對於第一年,其最優決策表如下:
    0 1 2 3   4   5   6   7   8   9   10(*1000)  -- (1)
    0 0 0 0   400 400 400 400 800 800 800        -- (2)
    0 0 0 250 400 400 500 650 800 900 900        -- (3)
    (1) -- 表示首先選利息爲400的債卷在對應資金下的最優利息。
    (2) -- 表示可用來購買債卷的資金。
    (3) -- 表示在已有狀態下再選擇利息爲300的債卷在對應資金下的最優利息。
    注意上面表格,在求購買利息爲300的債卷獲得的最優收益的時候,
    參考了以前的最優狀態,以3行8列的650爲例,7(*1000)可以
    在以前購買了0張4000的債卷的基礎上再2張3000的,也可以在以前購
    買了1張4000的基礎上再買1張3000,經比較取其收益大的,這就是典
    型的動態規劃中的當前最優狀態計算。
    本程序中把上面的最優決策二維表用一個一維數組表示,值得借鑑

void add(int a,int b)
{ cout << a << " " << b << endl; // for debug
 for(int i=0;i<=80000;i++)
 {
  if(i+a > 80000)
  {
   break;
  }

  if(saifa[i]+b > saifa[i+a]) // 累計同時購買多種債卷時的利息
  {
   saifa[i+a] = saifa[i] + b;
  }

  if(i<200) // for debug
   cout << i << "-" << saifa[i] << " ";
 }
 cout << endl; // for debug
}

int main(void)
{
 int n,d,money,year,pay,bond;
 int ii,i;

 scanf("%d",&n);
 for(ii=0;ii<n;ii++)
 {
  memset(saifa,0,sizeof(saifa));
  scanf("%d%d",&money,&year);
  scanf("%d",&d);

  for(i=0;i<d;i++)
  {
   scanf("%d%d",&pay,&bond);
   add(pay/1000,bond);
  }

  // 計算指定年限內最優組合的本金利息總額
  for(i=0;i<year;i++)
  { cout << saifa[money/1000] << " "; // for debug
   money += saifa[money/1000]; 
  }
  cout << endl; // for debug

  printf("%d/n",money);
 }

 return 0;
}


8.最長公共子串問題
一個給定序列的子序列是在該序列中刪去若干元素後得到的序列。給定兩個序列X和Y,當另一序列Z既是X的子序列又是Y的子序列時,稱Z是序列X和Y的公共子序列。最長公共子串就是求給定兩個序列的一個最長公共子序列。例如,X=“ABCBDAB”,Y=“BCDB”是X的一個子序列。
定義c[i][j]爲序列“a0,a1,…,ai-2”和“b0,b1,…,bj-1”的最長公共子序列的長度,計算c[i][j]可遞歸地表述如下: 
(1)c[i][j] = 0                         如果i=0或j=0; 
(2)c[i][j] = c[i-1][j-1]+1             如果i,j>0,且a[i-1] = b[j-1]; 
(3)c[i][j] = max{c[i][j-1], c[i-1][j]} 如果i,j>0,且a[i-1] != b[j-1]。 
按此算式可寫出計算兩個序列的最長公共子序列的長度函數。由於c[i][j]的產生僅依賴於c[i-1][j-1]、c[i-1][j]和c[i][j-1],故可以從c[m][n]開始,跟蹤c[i][j]的產生過程,逆向構造出最長公共子序列。
細節見程序。 

#include <stdio.h>
#include <string.h>

#define N 100

char a[N], b[N], str[N];
int c[N][N];

int lcs_len(char* a, char* b, int c[][N])
{
    int m = strlen(a), n = strlen(b), i, j;

    for( i=0; i<=m; i++ )     
        c[i][0]=0;
    for( i=0; i<=n; i++ )     
        c[0][i]=0;

    for( i=1; i<=m; i++ )  
    {
        for( j=1; j<=n; j++ )
        {
            if (a[i-1]==b[j-1])
                c[i][j]=c[i-1][j-1]+1;
            else if (c[i-1][j]>=c[i][j-1])
                c[i][j]=c[i-1][j];
            else
                c[i][j]=c[i][j-1];
        }
    }

    return c[m][n];
}

char* build_lcs(char s[], char* a, char* b)
{
    int i = strlen(a), j = strlen(b);
    int k = lcs_len(a,b,c);
    s[k] = '/0';
    while( k>0 )
    {
        if (c[i][j]==c[i-1][j])  
            i--;
        else if (c[i][j]==c[i][j-1])  
            j--;
        else
        { 
            s[--k]=a[i-1];
            i--; j--;
        }
    }

    return s;
}

void main()
{  
    printf("Enter two string (length < %d) :/n",N);
    scanf("%s%s",a,b);
    printf("LCS=%s/n",build_lcs(str,a,b)); 
}

代碼

/* 主題:最長公共子序列
* 作者:chinazhangjie
* 郵箱:[email protected]
* 開發語言:C++
* 開發環境:Microsoft Visual Studio 2008
* 時間: 2010.11.14
*/
#include <iostream>
#include <vector>
using namespace std ;

// longest common sequence
class LonComSequence
{
public:
typedef vector<vector<int> > LCS_Type ;
typedef vector<vector<int> > MarkType ;

public:
LonComSequence (const vector<char>& vSeq1,
const vector<char>& vSeq2)
: mc_nEqual (1), mc_nSq1move(2), mc_nSq2move(3)
{
m_vSeq1 = vSeq1 ;
m_vSeq2 = vSeq2 ;
m_nLen1 = vSeq1.size() ;
m_nLen2 = vSeq2.size() ;

// 初始化最長公共子序列的長度
m_lcsLen.resize (m_nLen1 + 1) ;
m_mtMark.resize (m_nLen1 + 1) ;
for (int i = 0; i < m_nLen1 + 1; ++ i) {
m_lcsLen[i].resize (m_nLen2 + 1) ;
m_mtMark[i].resize (m_nLen2 + 1) ;
}
}

// 計算最長公共子序列的長度
int calLcsLength ()
{
for (int i = 1; i <= m_nLen1; ++ i) {
m_lcsLen[i][0] = 0 ; // 序列二的長度爲0,公共子序列的長度爲0
}
for (int i = 1; i <= m_nLen2; ++ i) {
m_lcsLen[0][i] = 0 ; // 序列一的長度爲0,公共子序列的長度爲0
}

for (int i = 0; i < m_nLen1; ++ i) {
for (int j = 0; j < m_nLen2; ++ j) {
if (m_vSeq1[i] == m_vSeq2[j]) {
m_lcsLen[i+1][j+1] = m_lcsLen[i][j] + 1 ;
m_mtMark[i+1][j+1] = mc_nEqual ;
}
else if (m_lcsLen[i][j+1] >= m_lcsLen[i+1][j]) {
m_lcsLen[i+1][j+1] = m_lcsLen[i][j+1] ;
m_mtMark[i+1][j+1] = mc_nSq1move ;
}
else {
m_lcsLen[i+1][j+1] = m_lcsLen[i+1][j] ;
m_mtMark[i+1][j+1] = mc_nSq2move ;
}
}
}
return m_lcsLen[m_nLen1][m_nLen2] ;
}
// 構造最長公共子序列
void LCS() {
cout << "LCS is : " ;
__LCS(m_nLen1, m_nLen2);
cout << endl ;
}

private:
void __LCS (int i, int j)
{
if (i == 0 || j == 0) {
return ;
}

if (m_mtMark[i][j] == mc_nEqual) {
__LCS (i - 1, j - 1) ;
cout << m_vSeq1[i - 1] << " " ;
}
else if (m_mtMark[i][j] == mc_nSq1move) {
__LCS (i - 1, j) ;
}
else {
__LCS (i, j - 1) ;
}
}

private:
vector<char> m_vSeq1 ; // 序列一
vector<char> m_vSeq2 ; // 序列二
int m_nLen1 ; // 序列一的長度
int m_nLen2 ; // 序列二的長度
LCS_Type m_lcsLen ; // 最長公共子序列的長度
MarkType m_mtMark ; // 記錄m_lcsLen
const int mc_nEqual ; // 相等的標誌
const int mc_nSq1move ; // 序列一左移的標誌
const int mc_nSq2move ; // 序列二左移的標誌
} ;


int main()
{
vector<char> s1 ;
s1.push_back ('A') ;
s1.push_back ('B') ;
s1.push_back ('C') ;
s1.push_back ('D') ;
s1.push_back ('E') ;
s1.push_back ('F') ;

vector<char> s2 ;
s2.push_back ('B') ;
s2.push_back ('D') ;
s2.push_back ('F') ;
s2.push_back ('G') ;
s2.push_back ('H') ;

LonComSequence lcs(s1, s2) ;
cout << lcs.calLcsLength () << endl ;
lcs.LCS();

return 0 ;
}


9.矩陣連乘問題

題目

給定n個矩陣{A1,A2,...,An},其中Ai與Ai+1是可乘的,i = 1,2, ...n-1。考慮這n個矩陣的乘積。由於競爭乘法滿足結合律,故計算矩陣的連乘有許多不同的計算次序。

這種計算次序可以用加括號的方式確定。若一個矩陣連乘的計算次序完全確定,這是就說該連乘已完全加括號。

問題可遞歸定義: 

(1)單個矩陣是完全加括號的;

(2)矩陣連乘積A是完全加括號的 ,則A可表示爲2個完全加括號的矩陣連乘積B和C的乘積並加括號,即 A = (BC)。

例如,矩陣連乘A1 *A2 *A3 *A4 可以有5種完全加括號的方式:(A1 *(A2 *(A3 *A4 ))), (A1 *((A2 *A3) *A4)),((A1 *A2 )*(A3 *A4)),(((A1 *A2 )*A3 )*A4)。每種加括號的方式確定了一個計算的次序。不同的計算次序與矩陣連乘的計算量有密切的關係。關於矩陣如何相乘這裏我就不贅述了請看about matrix 

考慮3個矩陣{A1,A2,A3}連乘的例子,假設這3個矩陣的維數分別爲 10×100, 100×5, 5×50。若按照((A1*A2)*A3)計算,則計算次數爲10×100×5 + 10×5×50 = 7500

若按(A1*(A2*A3))計算,則計算次數爲   100×5×50 + 10×100×50 = 75000。第1種方法的計算次數是後者的10倍!由此可以看出,不同的加括號方式確定不同的計算次序對矩陣乘法的運算量影響是巨大的。

矩陣連乘爲題定義如下:給定n個矩陣{A1,A2,...,An},矩陣A1的維數爲pi-1×pi, i = 1,2, ..., n,如何給矩陣連乘A1*A2*....*An完全加上括號使用矩陣乘法中計算次數最少。

問題分析

窮舉法:列舉出所有可能的計算次序,並計算出每一種計算次序相應需要的數乘次數,從中找出一種數乘次數最少的計算次序。

算法複雜度分析:

對於n個矩陣的連乘積,設其不同的計算次序爲P(n)

由於每種加括號方式都可以分解爲兩個子矩陣的加括號問題

(A1...Ak)(A(k+1)…An)可以得到關於P(n)的遞推式如下:

動態規劃:將矩陣連乘積A(i)A(i+1)…A(j)簡記爲A[i:j],這裏 i <= j。

考察計算A[i:j]的最優計算次序。設這個計算次序在矩陣A(k)和A(k+1)之間將矩陣鏈斷開,i <= k < j, 則其相應完全加括號方式爲(A(i)A(i+1)...A(k)) * (A(k+1)A(k+2)...A(j))。

計算量:A[i:k]的計算量加上A[k+1,j],再加上A[i:k] * A[k+1][j]的計算量。

分析最優解的結構 

特徵:計算A[i:j]的最優次序所包含的計算矩陣子鏈 A[i:k]和A[k+1:j]的次序也是最優的。

矩陣連乘計算次序問題的最優解包含着其子問題的最優解。這種性質稱爲最優子結構性質問題的最優子結構性質是該問題可用動態規劃算法求解的顯著特徵

建立遞歸關係 

設計算A[i:j],1 <= i <= j <= n,所需要的最少數乘次數m[i,j],則原問題的最優值爲m[1,n]

當i = j時,A[i:j]=Ai,因此,m[i,i] = 0,i = 1,2,…,n

當i < j時,m[i,j] = m[i,k] + m[k+1,j] + p(i-1)p(k)p(j)

這裏A(i)的維數爲p(i-1)*(i)(注:p(i-1)爲矩陣A(i)的行數,p(i)爲矩陣A[i]的列數)

可以遞歸地定義m[i,j]爲:

k的位置只有j - i種。

計算最優值 

對於1 <= i <= j <= n不同的有序對(i,j)對應於不同的子問題。因此,不同子問題的個數最多隻有:

(大括號表示C(n,2),組合的意思。後面的符號表示 “緊漸近界記號”)

但是,在遞歸計算時,許多子問題被重複計算多次。這也是該問題可用動態規劃算法求解的又一顯著特徵。

用動態規劃算法解此問題,可依據其遞歸式以自底向上的方式進行計算。在計算過程中,保存已解決的子問題答案。每個子問題只計算一次,而在後面需要時只要簡單查一下,從而避免大量的重複計算,最終得到多項式時間的算法。

用動態規劃法求最優解 

連乘矩陣假如爲:

計算過程爲:

從m可知最小連乘次數爲m[1][6] = 15125

從s可知計算順序爲((A1(A2A3))((A4A5))A6))

實現:

#include<iostream>
void main()
{
        int m[8][8], min;
        int r[8] = {10, 20, 50, 1, 100, 4, 20, 2};     /* 矩陣維數 */
  
        /* 初始化 */
       memset(m,0,sizeof(m));
        /* 每此增量加一 */
        for (int l = 1; l < 7; l++)
       {
              /* 對於差值爲 l 的兩個元素 */
              for (int i = 1; i <= 7 - l; i++)
             {
                  j = i + l;
                  /* 求其最小組合方式 */
                  min = m[i][i] + m[i+1][j] + r[i-1] * r[i] * r[j];
                  middle[i][j] = i;
                  for (int k = i + 1; k < j; k++)
                 {
                       if (min > m[i][k] + m[k+1][j] + r[i-1] * r[k] * r[j])
        {
                               min = m[i][k] + m[k+1][j] + r[i-1] *r[k]* r[j];
                                        middle[i][j] = k;
                        }
                  }
                        m[i][j] = min;
             }
        }
        std::cout<<m[1][N];
}

由以上代碼可以很容易看出算法的時間複雜度爲O(n^3)。即便如此也比窮舉法的指數級時間複雜度快。

代碼

/* 主題:矩陣連乘問題
* 作者:chinazhangjie
* 郵箱:[email protected]
* 開發語言:C++
* 開發環境:Mircosoft Virsual Studio 2008
* 時間: 2010.11.14
*/

#include <iostream>
#include <vector>
using namespace std ;

class matrix_chain
{
public:
matrix_chain(const vector<int> & c) {
cols = c ;
count = cols.size () ;
mc.resize (count) ;
s.resize (count) ;
for (int i = 0; i < count; ++ i) {
mc[i].resize (count) ;
s[i].resize (count) ;
}
for (int i = 0; i < count; ++ i) {
for (int j = 0; j < count; ++ j) {
mc[i][j] = 0 ;
s[i][j] = 0 ;
}
}
}

// 使用備忘錄方法計算
void lookup_chain () {
__lookup_chain (1, count - 1) ;
min_count = mc[1][count - 1] ;
cout << "min_multi_count = "<< min_count << endl ;
// 輸出最優計算次序
__trackback (1, count - 1) ;
}

// 使用普通方法進行計算
void calculate () {
int n = count - 1; // 矩陣的個數
// r 表示每次寬度
// i,j表示從從矩陣i到矩陣j
// k 表示切割位置
for (int r = 2; r <= n; ++ r) {
for (int i = 1; i <= n - r + 1; ++ i) {
int j = i + r - 1 ;
// 從矩陣i到矩陣j連乘,從i的位置切割,前半部分爲0
mc[i][j] = mc[i+1][j] + cols[i-1] * cols[i] * cols[j] ;
s[i][j] = i ;
for (int k = i + 1; k < j; ++ k) {
int temp = mc[i][k] + mc[k + 1][j] +
cols[i-1] * cols[k] * cols[j] ;
if (temp < mc[i][j]) {
mc[i][j] = temp ;
s[i][j] = k ;
}
} // for k
} // for i
} // for r
min_count = mc[1][n] ;
cout << "min_multi_count = "<< min_count << endl ;
// 輸出最優計算次序
__trackback (1, n) ;

}

private:
int __lookup_chain (int i, int j) {
// 該最優解已求出,直接返回
if (mc[i][j] > 0) {
return mc[i][j] ;
}
if (i == j) {
return 0 ; // 不需要計算,直接返回
}

// 下面兩行計算從i到j按照順序計算的情況
int u = __lookup_chain (i, i) + __lookup_chain (i + 1, j)
+ cols[i-1] * cols[i] * cols[j] ;
s[i][j] = i ;
for (int k = i + 1; k < j; ++ k) {
int temp = __lookup_chain(i, k) + __lookup_chain(k + 1, j)
+ cols[i - 1] * cols[k] * cols[j] ;
if (temp < u) {
u = temp ;
s[i][j] = k ;
}
}
mc[i][j] = u ;
return u ;
}

void __trackback (int i, int j) {
if (i == j) {
return ;
}
__trackback (i, s[i][j]) ;
__trackback (s[i][j] + 1, j) ;
cout <<i << "," << s[i][j] << " " << s[i][j] + 1 << "," << j << endl;
}

private:
vector<int> cols ; // 列數
int count ; // 矩陣個數 + 1
vector<vector<int> > mc; // 從第i個矩陣乘到第j個矩陣最小數乘次數
vector<vector<int> > s; // 最小數乘的切分位置
int min_count ; // 最小數乘次數
} ;

int main()
{
// 初始化
const int MATRIX_COUNT = 6 ;
vector<int> c(MATRIX_COUNT + 1) ;
c[0] = 30 ;
c[1] = 35 ;
c[2] = 15 ;
c[3] = 5 ;
c[4] = 10 ;
c[5] = 20 ;
c[6] = 25 ;

matrix_chain mc (c) ;
// mc.calculate () ;
mc.lookup_chain () ;
return 0 ;
}

算法複雜度分析: 

算法matrixChain的主要計算量取決於算法中對r,i和k的3重循環。循環體內的計算量爲O(1),而3重循環的總次數爲O(n^3)。因此算法的計算時間上界爲O(n^3)。算法所佔用的空間顯然爲O(n^2)。

10.最大子段和

問題表述 

n個數(可能是負數)組成的序列a1,a2,…an.求該序列

例如:  序列(-2,11,-4,13,-5,-2) ,最大子段和:

       11 - 4 + 13=20。

1)窮舉算法: O(n3), O(n2)

2)分治法:

將序列a[1:n]從n/2處截成兩段:a[1:n/2], a[n/2+1:n]


一共存在三種情況:

a.最大子段和出現在左邊一段

b.最大子段和出現在右邊一段

c.最大子段和跨越中間的斷點

對於前兩種情況,只需繼續遞歸調用,而對於第三種情況:

那麼S1+S2是第三種情況的最優值。

3)動態規劃法:

定義b[j]:

含義:從元素i開始,到元素j爲止的所有的元素構成的子段有多個,這些子段中的子段和最大的那個。

那麼:

如果:b[j-1] > 0, 那麼b[j] = b[j-1] + a[j]

如果:b[j-1] <= 0,那麼b[j] = a[j]

這樣,顯然,我們要求的最大子段和,是b[j]數組中最大的那個元素。

實現:

代碼

/* 主題:最大子段和
* 作者:chinazhangjie
* 郵箱:[email protected]
* 開發語言:C++
* 開發環境:Microsoft Virsual Studio 2008
* 時間: 2010.11.15
*/

#include <iostream>
#include <vector>
using namespace std ;

class MaxSubSum
{
public:
MaxSubSum (const vector<int>& intArr)
{
m_vIntArr = intArr ;
m_nLen = m_vIntArr.size () ;
}

// use divide and conquer
int use_DAC ()
{
m_nMssValue = __use_DAC (0, m_nLen - 1) ;
return m_nMssValue ;
}

// use dynamic programming
int use_DP ()
{
int sum = 0 ;
int temp = 0 ;

for (int i = 0; i < m_nLen; ++ i) {
if (temp > 0) {
temp += m_vIntArr[i] ;
}
else {
temp = m_vIntArr[i] ;
}
if (temp > sum) {
sum = temp ;
}
}
m_nMssValue = sum ;
return sum ;
}

private:
int __use_DAC (int left, int right)
{
// cout << left << "," << right << endl ;
if (left == right) {
return (m_vIntArr[left] > 0 ? m_vIntArr[left] : 0) ;
}

// 左邊區域的最大子段和
int leftSum = __use_DAC (left, (left + right) / 2) ;
// 右邊區域的最大子段和
int rightSum = __use_DAC ((left + right) / 2 + 1, right) ;
// 中間區域的最大子段和
int sum1 = 0 ;
int max1 = 0 ;
int sum2 = 0 ;
int max2 = 0 ;
for (int i = (left + right) / 2; i >= left; -- i) {
sum1 += m_vIntArr[i] ;
if (sum1 > max1) {
max1 = sum1 ;
}
}
for (int i = (left + right) / 2 + 1; i <= right; ++ i) {
sum2 += m_vIntArr[i] ;
if (sum2 > max2) {
max2 = sum2 ;
}
}
int max0 = max1 + max2 ;
max0 = (max0 > 0 ? max0 : 0) ;
// cout << max0 << ", " << leftSum << ", " << rightSum << endl ;
return max (max0 , max (leftSum, rightSum)) ;
}

private:
vector<int> m_vIntArr ; // 整形序列
int m_nLen ; // 序列長度
int m_nMssValue;// 最大子段和
} ;

int main()
{
vector<int> vArr ;
vArr.push_back (-2) ;
vArr.push_back (11) ;
vArr.push_back (-4) ;
vArr.push_back (13) ;
vArr.push_back (-5) ;
vArr.push_back (-2) ;

MaxSubSum mss (vArr) ;
cout << mss.use_DP () << endl ;
return 0 ;
}

11.多邊形遊戲

多邊形遊戲是一個單人玩的遊戲,開始時有一個由n個頂點構成的多邊形。每個頂點被賦予一個整數值,每條邊被賦予一個運算符”+”或”*”。所有邊依次用整數從1到n編號。

遊戲第1步,將一條邊刪除。

隨後n-1步按以下方式操作:

(1)選擇一條邊E以及由E連接着的2個頂點V1和V2;

(2)用一個新的頂點取代邊E以及由E連接着的2個頂點V1和V2。將由頂點V1和V2的整數值通過邊E上的運算得到的結果賦予新頂點。

最後,所有邊都被刪除,遊戲結束。遊戲的得分就是所剩頂點上的整數值。

問題對於給定的多邊形,計算最高得分。

最優子結構性質

按照順時針順序,多邊形和頂點的順序可以寫成:

    op[1], v[1], op[2], v[2], , op[n], v[n]

在所給多邊形中,從頂點i(1 <= i <= n)開始,長度爲j(鏈中有j個頂點)的順時針鏈p(ij) 可表示爲

v[i], op[i+1], v[i+1],…, op[i+j-1], v[i+j-1]

如果這條鏈在op[i + s]處進行最後一次合併運算(1 <= s <= j-1),則可在op[i+s]處將鏈分割爲2個子鏈:

i開始長度爲s的鏈:  p(is)

i + s開始,長度爲j - s的鏈:p(i + sj-s)

:

m1是對子鏈p(i,s)的任意一種合併方式得到的值,而a和b分別是在所有可能的合併中得到的最小值和最大值。

m2是p(i+s,j-s)的任意一種合併方式得到的值,而c和d分別是在所有可能的合併中得到的最小值和最大值。

依此定義有a <= m1 <= b,c <= m2 <= d

(1)當op[i+s] = ‘+’時,顯然有a + c <= m <= b + d

(2)當op[i+s] = ’*’時,有

min {acadbcbd} <= m <= max {acadbcbd}

換句話說,主鏈的最大值和最小值可由子鏈的最大值和最小值得到

實現:

 

代碼

/* 主題:多邊形遊戲
* 作者:chinazhangjie
* 郵箱:[email protected]
* 開發語言:C++
* 開發環境:Vicrosoft Visual Studio
* 時間: 2010.11.15
*/

#include <iostream>
#include <vector>
using namespace std ;

struct SegInfo
{
public:
SegInfo ()
: m_nMaxValue (0), m_nMinValue(0)
{}
SegInfo (int maxValue, int minValue)
: m_nMaxValue (maxValue), m_nMinValue (minValue)
{}
public:
int m_nMaxValue ;
int m_nMinValue ;
} ;

class PolyGame
{
public:
PolyGame (const vector<char>& op, const vector<int>& vertex)
{
m_vcOp = op ;
m_vnVertex = vertex ;
m_nCount = m_vcOp.size () ;

m_vSeg.resize (m_nCount) ;
for (int i = 0; i < m_nCount; ++ i) {
m_vSeg[i].resize (m_nCount) ;
}
}

int beginCalulate ()
{
// 初始邊界
for (int i = 1; i < m_nCount; ++ i) {
m_vSeg[i][1].m_nMaxValue = m_vnVertex[i] ;
m_vSeg[i][1].m_nMinValue = m_vnVertex[i] ;
}

// i: 起點
// j: 長度
// s: 子切分位置
for (int j = 2; j < m_nCount ; ++ j) {
for (int i = 1; i < m_nCount; ++ i) {
for (int s = 1; s < j; ++ s) {
SegInfo si = __calMinAndMax(i, s, j) ;
if (m_vSeg[i][j].m_nMinValue > si.m_nMinValue) {
m_vSeg[i][j].m_nMinValue = si.m_nMinValue ;
}
if (m_vSeg[i][j].m_nMaxValue < si.m_nMaxValue) {
m_vSeg[i][j].m_nMaxValue = si.m_nMaxValue ;
}
}
}
}
// 找到最大值
int temp = m_vSeg[1][m_nCount - 1].m_nMaxValue ;
for (int i = 2; i < m_nCount; ++ i) {
if (temp < m_vSeg[i][m_nCount - 1].m_nMaxValue) {
temp = m_vSeg[i][m_nCount - 1].m_nMaxValue ;
}
}
m_nResult = temp ;
return m_nResult ;
}

private:
// 從i開始,長度爲j,s爲切分位置
SegInfo __calMinAndMax (int i, int s, int j)
{
int minL = 0 ;
int maxL = 0 ;
int minR = 0 ;
int maxR = 0 ;
minL = m_vSeg[i][s].m_nMinValue ;
maxL = m_vSeg[i][s].m_nMaxValue ;
int r = (i + s - 1) % (m_nCount - 1) + 1 ;
minR = m_vSeg[r][j - s].m_nMinValue ;
maxR = m_vSeg[r][j - s].m_nMaxValue ;

SegInfo si ;
// 處理加法
if (m_vcOp[r] == '+') {
si.m_nMinValue = minL + minR ;
si.m_nMaxValue = maxL + maxR ;
}
else { // 處理乘法
vector<int> mm ;
mm.push_back (minL * minR) ;
mm.push_back (minL * maxR) ;
mm.push_back (maxL * minR) ;
mm.push_back (maxL * maxR) ;
int min = 0 ;
int max = 0 ;
for (vector<int>::iterator ite = mm.begin();
ite != mm.end() ; ++ ite) {
if (*ite < min) {
min = *ite ;
}
if (*ite > max) {
max = *ite ;
}
}
si.m_nMinValue = min ;
si.m_nMaxValue = max ;
}
return si ;
}

private :
vector<char> m_vcOp ; // 運算符(下標從1開始)
vector<int> m_vnVertex ;// 頂點值(下標從1開始)
int m_nCount ; // 邊的個數
int m_nResult ; // 結果
vector<vector<SegInfo> > m_vSeg ;// 合併後的信息
} ;

int main()
{
const int cnCount = 5 ;
vector<char> op (cnCount + 1);
vector<int> vertex (cnCount + 1);
op[1] = '+' ;
op[2] = '*' ;
op[3] = '+' ;
op[4] = '*' ;
op[5] = '*' ;

vertex[1] = 10 ;
vertex[2] = -8 ;
vertex[3] = 3;
vertex[4] = -2 ;
vertex[5] = -1 ;

PolyGame pg (op, vertex) ;
cout << pg.beginCalulate () << endl ;
}

12.最長遞增字串 

問題描述:http://en.wikipedia.org/wiki/Longest_increasing_subsequence_problem   (其實顯而易見)

假設我的一串input是 X1,X2,X3,。。。,Xn,按下圖構造我們的樹(enumeration tree)。

 菜鳥學算法——動態規劃(二) - IMAX - IMAX 的博客

如何構造這棵樹通過上面這個圖,我想應該顯而易見了吧,最終我的的解一定是在某個葉子節點上的。

在這裏,最主要的是找到prunning rules。

當然對於這個LIS問題來說,什麼纔是我們的prunning rules呢?

1)很顯然,如果某個節點的序列不是遞增的,我們應該直接剪掉,因爲。。。顯然啊~

2)對於同一層的節點來說,如果同一層的某兩個節點具有相同的遞增長度,我們應該保留 那個序列,他的最後一個元素的數值最小。(怎麼感覺說的這麼彆扭。怒)比如在某一層上  一個節點是 2 4 7, 另一個節點是 3 4 6,他們的遞增長度都是3,但是因爲6<7,所以我們應該保留3 4 6,把2 4 7這個節點直接剪掉。

3)對於同一層的節點來說,如果某兩個節點最後一個元素的數值一樣,我們應該保留長度較長的那個節點,剪掉較短的那個。舉例:一個節點  2  3  6,一個節點  3  6, 我們保留 2 3 6,剪掉 3 6。

其實我們可以發現,(2)(3)實際上說的是一回事,所以,我們可以根據(1)(2)寫出我們的DP,也可以根據(1)(3)寫出我們的DP。

神馬?這樣就可以了?Yes, it's too simple, sometimes naive???!!!

根據(1)(2)

A[l,s] 這個是我們table A裏面的一個cell,l表示樹的level,s表示長度,A[l,s]表示在該節點的最後一個元素的數值。

for l = 1 to n

  for s = 1 to l

    if A[l,s] then

      A[l+1,s] = min(A[l+1,s], A[l,s])

    if A[l,s] < X_{l+1} then

      A[l+1,s+1] = min(A[l+1,s+1], X_l+1)

根據(1)(3)

A[l,s] 這個是我們table A裏面的一個cell,l表示樹的level,s表示該節點的最後一個元素的下標,即最後一個元素是Xs,A[l,s]表示長度。

for l = 1 to n

  for s = 1 to l

    if A[l,s] then

      A[l+1,s] = max(A[l+1,s], A[l,s])

    if Xs < X_{l+1} //如果在l+1層新添加的元素可以增加我們的序列的長度

      //A[l+1,s]+1 表示在原來的基礎上在加上這個新元素

      //A[l+1,l+1] 表示在以新來的的元素X_{l+1}結尾的序列中,找一個最長的出來。

      A[l+1,l+1] = max(A[l+1,l+1], A[l+1,s]+1)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章