初探動態(tài)規(guī)劃算法

概念

維基百科的定義如下:

dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions.

從中我們知道動態(tài)規(guī)劃關(guān)注三點:

  1. 把一個問題劃分為若干相似的子問題
  2. 所有的子問題只需要解決一次
  3. 存儲子問題的解

動態(tài)規(guī)劃所涉及的幾個重要概念也如下所示:

  1. 最優(yōu)子結(jié)構(gòu):每個階段的最優(yōu)狀態(tài)可以從之前某個階段的某個或某些狀態(tài)得到。即思考大問題的最優(yōu)解是如何由小問題的最優(yōu)解得到的。
  2. 無后效性:如果給定某一階段的狀態(tài),則在這一階段以后過程的發(fā)展不受此階段以前各段狀態(tài)的影響 - “未來和過去無關(guān)”
  3. 邊界:通常是問題的結(jié)束條件
  4. 狀態(tài)轉(zhuǎn)移公式:說明了問題的每一階段與上一個/一些階段的相互關(guān)系
  5. 子問題重疊性質(zhì):在用遞歸算法自頂向下對問題進行求解時,每次產(chǎn)生的子問題并不總是新問題,有些子問題會被重復(fù)計算多次。動態(tài)規(guī)劃算法對此進行了優(yōu)化,對每個子問題只需要計算一次,把計算結(jié)果存儲在表格中,便于下次使用

算法設(shè)計

一個動態(tài)規(guī)劃算法基本可以分為以下步驟:

  1. 從題目中確定最優(yōu)子結(jié)構(gòu)是什么
  2. 確定問題的邊界條件
  3. 根據(jù)上述兩步構(gòu)建數(shù)學(xué)模型,得到相應(yīng)的狀態(tài)轉(zhuǎn)移方程
  4. 根據(jù)數(shù)學(xué)模型進行代碼編寫

例題一

有一座高度是10級臺階的樓梯,從下往上走,每跨一步只能向上1級或者2級臺階。要求用程序來求出一共有多少種走法。

解析

假設(shè)0-9級臺階共有X個走法,0-8級臺階共有Y個走法,則總共的走法共有X+Y個走法,如下圖所示:

臺階示意圖.png

對于8級臺階到10臺階,只存在跨越2步這個可能,因為若到了8級臺階之后,每次跨越1步,就到了9級臺階,此種走法包含到了9級臺階的X走法之中。
綜上可知,到9級臺階的所有走法由到第8級臺階和第7級臺階組成….,以此類推。
無后效性體現(xiàn)在8級臺階之后的所有走法不受以前各級走法的影響。

數(shù)學(xué)建模

使用數(shù)學(xué)公式可表示為 F (10) = F (9) + F (8) ,其可以看作為最優(yōu)子結(jié)構(gòu),則以此類推 F (9) = F (8) + F (7)…, 從而可以得到如下公式:
? F (1) = 1;
? F (2) = 2;
? F (n) = F (n-1) + F (n-2);
從上面的公式可以看出 F (1) = 1,F (2) = 2 稱為問題的邊界,若一個問題沒有邊界,則永遠(yuǎn)無法得到有限的結(jié)果,F (n) = F (n-1) + F (n-2) 是狀態(tài)轉(zhuǎn)移方程,說明了問題的每一階段與上一個/一些階段的相互關(guān)系。

代碼實現(xiàn)

1. 遞歸實現(xiàn)

int getClimbingWays(int n){
        if (n < 1){
            return 0;
        }
        if (n == 1){
            return 1;
        }
        if (n == 2){
            return 2;
        }
        return getClimbingWays(n-1) + getClimbingWays(n - 2);
    }

如下圖遞歸的次數(shù)類似形成如下二叉樹,每個節(jié)點表示遞歸方法所計算的次數(shù),二叉樹高度為N-1,節(jié)點個數(shù)接近2的N-1次方個,隨遞歸方法的時間復(fù)雜度為O(N^2)。

遞歸圖.png

2. 備忘錄算法

使用遞歸算法有大量的重復(fù)計算,就像下圖所示,

遞歸圖.png
int getClimbingWays(int n, HashMap<Integer, Integer> memo){
        if (n < 1){
            return 0;
        }
        if (n == 1){
            return 1;
        }
        if (n == 2){
            return 2;
        }
        if (memo.containsKey(n)){
            return memo.get(n);
        }else {
            int value =  getClimbingWays(n -1,memo) + getClimbingWays(n -2, memo);
            memo.put(n,value);
            return value;
        }
    }

此時的時間和空間復(fù)雜度都為O(N)

3. 動態(tài)規(guī)劃算法

就算備忘錄算法對算法進行了優(yōu)化,但是其還是要保持所有的子狀態(tài),造成空間復(fù)雜度過高,并且遞歸算法和備忘錄算法都是自頂向下進行處理,即從 F (N) 慢慢迭代到 F (1)F (2) ,現(xiàn)嘗試自底向上進行求解,只保存當(dāng)前狀態(tài)的前兩個狀態(tài)。分析過程如下:

  1. F (1)和F (2)為已知道結(jié)果,第一次迭代后,臺階數(shù)為3,走法數(shù)量為3,可知 F (3) 只依賴 F (2)F (2),可得下表
第一次迭代.png
  1. 第二次迭代后,臺階數(shù)為4,走法為5,可知 F (4) 只依賴于 F (3)F (2)
第二次迭代.png

其他迭代也如上所示,可知在每次迭代過程中,只需要保存前兩個狀態(tài)即可。

static int getClimbingWays(int n){
        if (n < 1){
            return 0;
        }
        if(n == 1){
            return 1;
        }
        if (n == 2){
            return 2;
        }
        int a = 1;
        int b = 2;
        int ResultWays = 0;
        for (int i = 3; i <= n; i++){
            ResultWays = a + b;
            a = b;
            b = ResultWays;
        }
        return ResultWays;
    }

動態(tài)規(guī)劃算法的時間復(fù)雜度為O(N),空間復(fù)雜度為O(1)。

例題二

有一個國家發(fā)現(xiàn)了5座金礦,每座金礦的黃金儲量不同,需要參與挖掘的工人數(shù)也不同。參與挖礦工人的總數(shù)是10人。每座金礦要么全挖,要么不挖,不能派出一半人挖取一半金礦。要求用程序求解出,要想得到盡可能多的黃金,應(yīng)該選擇挖取哪幾座金礦?
其中金礦1:400金/5人,金礦2:500金/5人,金礦3:200金/3人,金礦4:300金/4人,金礦5:350金/3人

解析

我們的最終要求解的問題是:10人5金礦時的最優(yōu)選擇,我們可以先假設(shè)最優(yōu)子結(jié)構(gòu)為10個人4個金礦挖出最多黃金,但是第五個金礦存在挖或者不挖的可能性,遂可進行擴展分為兩個最優(yōu)子結(jié)構(gòu):

  1. 第五個金礦不挖,最優(yōu)子結(jié)構(gòu)為10個人4個金礦挖出最多黃金
  2. 第五個金礦挖,最優(yōu)子結(jié)構(gòu)為10 - 第五個金礦所需人數(shù)時挖4個金礦的最優(yōu)選擇 + 第五個金礦的黃金儲量

則五個金礦的最優(yōu)選擇就是(10個人4個金礦的最優(yōu)選擇)和(10 - 第五個金礦所需人數(shù)時挖4個金礦的最優(yōu)選擇 + 第五個金礦的黃金儲量)的最大值。
邊界分為兩種情況,說明如下:

  1. 只有一個金礦,并且工人數(shù)滿足金礦所需人數(shù)要求,遂得到黃金數(shù)量為第
    一個金礦的儲量
  2. 只有一個金礦,若工人數(shù)不滿足金礦所需人數(shù)要求,則得到的黃金數(shù)量為0

數(shù)學(xué)建模

金礦數(shù)量 = N ,工人數(shù)量 = W ,金礦黃金量 G [] ,每個金礦的用工數(shù)量 P [] 。數(shù)組下標(biāo)都從0開始,則5座金礦和4座金礦的最優(yōu)選擇之間存在如下關(guān)系: F (5,10) = MAX (F (4,10), F (4,10-P [4]) +G (4) ) 。可以得到如下狀態(tài)轉(zhuǎn)移方程:
? F (N W ) =0 (N <= 1, W < P [0]) ; // 金礦數(shù)量小于1或一個金礦但是人數(shù)不足
? F (N,W ) = G [0] (N == 1, W >= P [0]) ; // 金礦數(shù)量為1個,需要挖礦人數(shù)符合
? F (N,W ) = F (N-1, W) (N > 1, W < P [N-1]) ; //金礦數(shù)量大于一個,但是剩余的挖礦人數(shù)已經(jīng)不滿足繼續(xù)挖礦
? F (N,W ) = MAX (F (N-1,W ), F (N-1,W-P [N-1]) +G (N -1) ) ; //金礦數(shù)量大于一個,剩余的挖礦人數(shù)滿足繼續(xù)挖礦要求

代碼實現(xiàn)

1. 遞歸實現(xiàn)

public static int getMostGold(int n, int w, int g[], int p[]){
        if (n == 1){
            return w < p[n-1] ? 0 : g[n-1];
        }
        if (w < p[n - 1]){
            return getMostGold(n-1,w,g,p);
        }
        return Math.max(getMostGold(n-1,w,g,p), getMostGold(n-1, w - p[n-1],g,p) + g[n-1]);
    }

遞歸實現(xiàn)的時間復(fù)雜度為O(2^N)。

2. 動態(tài)規(guī)劃實現(xiàn)

給出一個表格,表格的列表示金礦( N ),行表示工人數(shù)( W ),相對應(yīng)的值給定 NW 之后獲得的黃金數(shù)量。

  1. 得到第一行數(shù)據(jù)如下:
第一行數(shù)據(jù).png
  1. 當(dāng)工人數(shù)在5-9期間時,設(shè) S =5~9,F (2, S ) = MAX (F (1, S ), F (1, S -5) +500) , 其中都因為 S -5 < 5 ,則5~9格子中,黃金量為500。而當(dāng) _W = 10 _時,F (2, 10) = MAX (F (1, 10), F (1, 5) + 500) 為900。
第二行數(shù)據(jù).png
  1. 第三個金礦200儲量,需要3人,第四金礦300儲量,需要4人,第五金礦350
    儲量,需要3人,依次計算可得下表:
結(jié)果表.png

綜上可得出規(guī)律,每個格子的黃金量都是都前一行的一個或者兩個格子推導(dǎo)而來,例如3金礦8工人時,就來自于2金礦5工人+第三個金礦儲量和2金礦8工人,即MAX (F (2, 8 ), F (2, 5) +200) = MAX (500, 200 + 500) = 700。所以我們只需要存儲前一行的數(shù)據(jù),就可以推導(dǎo)出新的一行。

規(guī)律圖.png
public void getMostGold(int n, int w, int[] g, int[] p){
        int[] preResult = new int [w]; // 保存前一行結(jié)果
        int[] results = new int [w];  // 保存當(dāng)前結(jié)果
        // 填充第一個金礦的數(shù)據(jù)
        for (int i = 0; i < w; i++){
            if (i+1 < p[0]){
                preResult[i] = 0;  // 對應(yīng)數(shù)學(xué)模型 F(N W)=0 (N<=1,W<P[0]);
            }else {
                preResult[i] = g[0];  // 對應(yīng)數(shù)學(xué)模型 F(N,W)=G[0] (N==1,W>=P[0]);
            }
        }
        showResults(preResult);  // 展示第一行的數(shù)據(jù)
   
        //對其他金礦進行處理,從第二個金礦開始,外層循環(huán)時金礦數(shù)量,內(nèi)層循環(huán)時工人數(shù)
        for (int i = 1; i < n; i++){   
            for (int j = 0; j < w; j++){
                if (j + 1 < p[i]){
                    results[j] = preResult[j]; // 對應(yīng)數(shù)學(xué)模型 F(N,W)=F(N-1,W) (N>1,W<P[N-1]);
                }else if (j + 1 == p[i]){
                    results[j] = Math.max(preResult[j],0 + g[i]); // 特殊情況,擁有工人數(shù)剛好與要挖的下一個金礦的所需工人數(shù)相同 若要挖下一個金礦,則挖前一個金礦的人數(shù)為0
                }else {
                    results[j] = Math.max(preResult[j],preResult[j - p[i]] + g[i]); // 對應(yīng)數(shù)學(xué)模型 F(N,W)=MAX(F(N-1,W),F(N-1,W-P[N-1]+G(N-1));
                }
            }
            showResults(results);
            preResult = results.clone();
            // preResult = results; 不可直接進行引用的賦值
        }
    }
public void showResults(int[] results){
        for(int i:results){
            System.out.print(i+" ");
        }
        System.out.println();
}

動態(tài)規(guī)劃實現(xiàn)的時間復(fù)雜度為O(N*W),空間復(fù)雜度為O(W)。

總結(jié)

但是動態(tài)規(guī)劃算法在有些情況下不一定是最好的選擇,當(dāng)5個金礦1000個工人時,因為動態(tài)規(guī)劃的時間和空間復(fù)雜度與W成正比,而遞歸算法與W無關(guān),其時間和空間復(fù)雜度都不如遞歸算法來的好。

參考資料

[1]. https://mp.weixin.qq.com/s/3h9iqU4rdH3EIy5m6AzXsg
[2]. https://www.zhihu.com/question/23995189

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容