1. 程式人生 > >java演算法之動態規劃基本思想以及具體案例

java演算法之動態規劃基本思想以及具體案例

一、基本概念

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

二、基本思想與策略

    基本思想與分治法類似,也是將待求解的問題分解為若干個子問題(階段),按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的資訊。在求解任一子問題時,列出各種可能的區域性解,通過決策保留那些有可能達到最優的區域性解,丟棄其他區域性解。依次解決各子問題,最後一個子問題就是初始問題的解。

    由於動態規劃解決的問題多數有重疊子問題這個特點,為減少重複計算,對每一個子問題只解一次,將其不同階段的不同狀態儲存在一個二維陣列中。

    與分治法最大的差別是:適合於用動態規劃法求解的問題,經分解後得到的子問題往往不是互相獨立的(即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解)

三、適用的情況

能採用動態規劃求解的問題的一般要具有3個性質:

    (1) 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。

    (2) 無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。

   (3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃演算法同其他演算法相比就不具備優勢)

四、求解的基本步驟

     動態規劃所處理的問題是一個多階段決策問題,一般由初始狀態開始,通過對中間階段決策的選擇,達到結束狀態。這些決策形成了一個決策序列,同時確定了完成整個過程的一條活動路線(通常是求最優的活動路線)。如圖所示。動態規劃的設計都有著一定的模式,一般要經歷以下幾個步驟。

    初始狀態→│決策1│→│決策2│→…→│決策n│→結束狀態

圖1 動態規劃決策過程示意圖

    (1)劃分階段:按照問題的時間或空間特徵,把問題分為若干個階段。在劃分階段時,注意劃分後的階段一定要是有序的或者是可排序的,否則問題就無法求解。

    (2)確定狀態和狀態變數:將問題發展到各個階段時所處於的各種客觀情況用不同的狀態表示出來。當然,狀態的選擇要滿足無後效性。

    (3)確定決策並寫出狀態轉移方程:因為決策和狀態轉移有著天然的聯絡,狀態轉移就是根據上一階段的狀態和決策來匯出本階段的狀態。所以如果確定了決策,狀態轉移方程也就可寫出。但事實上常常是反過來做,根據相鄰兩個階段的狀態之間的關係來確定決策方法和狀態轉移方程

    (4)尋找邊界條件:給出的狀態轉移方程是一個遞推式,需要一個遞推的終止條件或邊界條件。

    一般,只要解決問題的階段狀態狀態轉移決策確定了,就可以寫出狀態轉移方程(包括邊界條件)。

實際應用中可以按以下幾個簡化的步驟進行設計:

    (1)分析最優解的性質,並刻畫其結構特徵。

    (2)遞迴的定義最優解。

    (3)以自底向上或自頂向下的記憶化方式(備忘錄法)計算出最優值

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

五、演算法實現的說明

    動態規劃的主要難點在於理論上的設計,也就是上面4個步驟的確定,一旦設計完成,實現部分就會非常簡單。

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

    (1)問題的階段 (2)每個階段的狀態

    (3)從前一個階段轉化到後一個階段之間的遞推關係

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

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

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

六、動態規劃演算法基本框架 1 for(j=1; j<=m; j=j+1) // 第一個階段 2 xn[j] = 初始值; 3 4 for(i=n-1; i>=1; i=i-1)// 其他n-1個階段 5 for(j=1; j>=f(i); j=j+1)//f(i)與i有關的表示式 6 xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])}; 8 9 t = g(x1[j1:j2]); // 由子問題的最優解求解整個問題的最優解的方案10 11 print(x1[j1]); 12 13 for(i=2; i<=n-1; i=i+115 { 17 t = t-xi-1[ji]; 18 19 for(j=1; j>=f(i); j=j+1)21 if(t=xi[ji])23 break;25 } 具體例子

用一個實際例子來體現動態規劃的演算法思想——硬幣找零問題。

硬幣找零問題描述:現存在一堆面值為 V1、V2、V3 … 個單位的硬幣,問最少需要多少個硬幣才能找出總值為 T 個單位的零錢?假設這一堆面值分別為 1、2、5、21、25 元,需要找出總值 T 為 63 元的零錢。

很明顯,只要拿出 3 個 21 元的硬幣就湊夠了 63 元了。

基於上述動態規劃的思想,我們可以從 1 元開始計算出最少需要幾個硬幣,然後再求 2 元、3元…每一次求得的結果都儲存在一個數組中,以後需要用到時則直接取出即可。那麼我們什麼時候需要這些子問題的解呢?如何體現出由子問題的解得到較大問題的解呢?

其實,在我們從 1 元開始依次找零時,可以嘗試一下當前要找零的面值(這裡指 1元)是否能夠被分解成另一個已求解的面值的找零需要的硬幣個數再加上這一堆硬幣中的某個面值之和,如果這樣分解之後最終的硬幣數是最少的,那麼問題就得到答案了。

單是上面的文字描述太抽象,先假定以下變數:

values[] : 儲存每一種硬幣的幣值的陣列
valueKinds :幣值不同的硬幣種類數量,即values[]陣列的大小
money : 需要找零的面值
coinsUsed[] : 儲存面值為 i 的紙幣找零所需的最小硬幣數

演算法描述:

當求解總面值為 i 的找零最少硬幣數 coinsUsed[ i ] 時,將其分解成求解 coinsUsed[ i– cents]和一個面值為cents元的硬幣,由於 i– cents < i , 其解 coinsUsed[ i– cents] 已經存在,如果面值為 cents 的硬幣滿足題意,那麼最終解 coinsUsed[ i ] 則等於 coinsUsed[ i– cents] 再加上 1(即面值為 cents)的這一個硬幣。

下面用程式碼實現並測試一下:
Java程式碼

    public class CoinsChange {    
        /**   
         * 硬幣找零:動態規劃演算法   
         *    
         * @param values   
         *            :儲存每一種硬幣的幣值的陣列   
         * @param valueKinds   
         *            :幣值不同的硬幣種類數量,即coinValue[]陣列的大小   
         * @param money   
         *            :需要找零的面值   
         * @param coinsUsed   
         *            :儲存面值為i的紙幣找零所需的最小硬幣數   
         */   
        public static void makeChange(int[] values, int valueKinds, int money,    
                int[] coinsUsed) {    
       
            coinsUsed[0] = 0;    
            // 對每一分錢都找零,即儲存子問題的解以備用,即填表    
            for (int cents = 1; cents <= money; cents++) {    
       
                // 當用最小幣值的硬幣找零時,所需硬幣數量最多    
                int minCoins = cents;    
       
                // 遍歷每一種面值的硬幣,看是否可作為找零的其中之一    
                for (int kind = 0; kind < valueKinds; kind++) {                 
                    // 若當前面值的硬幣小於當前的cents則分解問題並查表    
                    if (values[kind] <= cents) {    
                        int temp = coinsUsed[cents - values[kind]] + 1;    
                        if (temp < minCoins) {    
                            minCoins = temp;    
                        }    
                    }    
                }    
                // 儲存最小硬幣數    
                coinsUsed[cents] = minCoins;    
       
                System.out.println("面值為 " + (cents) + " 的最小硬幣數 : "   
                        + coinsUsed[cents]);    
            }    
        }    
            
        public static void main(String[] args) {    
       
            // 硬幣面值預先已經按降序排列    
            int[] coinValue = new int[] { 25, 21, 10, 5, 1 };    
            // 需要找零的面值    
            int money = 63;    
            // 儲存每一個面值找零所需的最小硬幣數,0號單元捨棄不用,所以要多加1    
            int[] coinsUsed = new int[money + 1];    
       
            makeChange(coinValue, coinValue.length, money, coinsUsed);    
        }    
    }   


0/1揹包問題的動態規劃法求解,前人之述備矣,這裡所做的工作,不過是自己根據理解實現了一遍,主要目的還是鍛鍊思維和程式設計能力,同時,也是為了增進對動態規劃法機制的理解和掌握。

值得提及的一個問題是,在用 JAVA 實現時, 是按演算法模型建模,還是用物件模型建模呢? 如果用演算法模型,那麼 揹包的值、重量就直接存入二個數組裡;如果用物件模型,則要對揹包以及揹包問題進行物件建模。思來想去,還是採用了物件模型,儘管心裡感覺演算法模型似乎更好一些。有時確實就是這樣,物件模型雖然現在很主流,但也不是萬能的,採用其它的模型和視角,或許可以得到更好的解法。

揹包建模:
Java程式碼

    public class Knapsack {      
            
        /** 揹包重量  */      
        private int weight;      
              
        /** 揹包物品價值  */      
        private int value;      
        /***   
         * 構造器   
         */      
        public Knapsack(int weight, int value) {      
            this.value = value;      
            this.weight = weight;      
        }      
        public int getWeight() {      
            return weight;      
        }      
              
        public int getValue() {      
            return value;      
        }      
              
        public String toString() {      
            return "[weight: " + weight + " " + "value: " + value + "]";        
        }      
    }       
      
    import java.util.ArrayList;      
        
    /**   
     * 求解揹包問題:   
     * 給定 n 個揹包,其重量分別為 w1,w2,……,wn, 價值分別為 v1,v2,……,vn   
     * 要放入總承重為 totalWeight 的箱子中,    
     * 求可放入箱子的揹包價值總和的最大值。   
     *    
     * NOTE: 使用動態規劃法求解 揹包問題   
     * 設 前 n 個揹包,總承重為 j 的最優值為 v[n,j], 最優解揹包組成為 b[n];   
     * 求解最優值:   
     * 1. 若 j < wn, 則 : v[n,j] = v[n-1,j];   
     * 2. 若  j >= wn, 則:v[n,j] = max{v[n-1,j], vn + v[n-1,j-wn]}。   
     *    
     * 求解最優揹包組成:   
     * 1. 若 v[n,j] > v[n-1,j] 則 揹包 n 被選擇放入 b[n],    
     * 2. 接著求解前 n-1 個揹包放入 j-wn 的總承重中,    
     *    於是應當判斷 v[n-1, j-wn] VS v[n-2,j-wn], 決定 揹包 n-1 是否被選擇。   
     * 3. 依次逆推,直至總承重為零。   
     *       
     *    重點: 掌握使用動態規劃法求解問題的分析方法和實現思想。   
     *    分析方法: 問題例項 P(n) 的最優解S(n) 蘊含 問題例項 P(n-1) 的最優解S(n-1);   
     *              在S(n-1)的基礎上構造 S(n)    
     *    實現思想: 自底向上的迭代求解 和 基於記憶功能的自頂向下遞迴   
     */      
    public class KnapsackProblem {      
              
        /** 指定揹包 */      
        private Knapsack[] bags;      
              
        /** 總承重  */      
        private int totalWeight;      
              
        /** 給定揹包數量  */      
        private int n;      
              
        /** 前 n 個揹包,總承重為 totalWeight 的最優值矩陣  */      
        private int[][] bestValues;      
              
        /** 前 n 個揹包,總承重為 totalWeight 的最優值 */      
        private int bestValue;      
              
        /** 前 n 個揹包,總承重為 totalWeight 的最優解的物品組成 */      
        private ArrayList<Knapsack> bestSolution;      
              
        public KnapsackProblem(Knapsack[] bags, int totalWeight) {      
            this.bags = bags;      
            this.totalWeight = totalWeight;      
            this.n = bags.length;      
            if (bestValues == null) {      
                bestValues = new int[n+1][totalWeight+1];      
            }      
        }      
              
        /**   
         * 求解前 n 個揹包、給定總承重為 totalWeight 下的揹包問題   
         *    
         */      
        public void solve() {      
                  
            System.out.println("給定揹包:");      
            for(Knapsack b: bags) {      
                System.out.println(b);      
            }      
            System.out.println("給定總承重: " + totalWeight);      
                  
            // 求解最優值      
            for (int j = 0; j <= totalWeight; j++) {      
                for (int i = 0; i <= n; i++) {      
                      
                    if (i == 0 || j == 0) {      
                        bestValues[i][j] = 0;      
                    }         
                    else       
                    {      
                        // 如果第 i 個揹包重量大於總承重,則最優解存在於前 i-1 個揹包中,      
                        // 注意:第 i 個揹包是 bags[i-1]      
                        if (j < bags[i-1].getWeight()) {      
                            bestValues[i][j] = bestValues[i-1][j];      
                        }         
                        else       
                        {      
                            // 如果第 i 個揹包不大於總承重,則最優解要麼是包含第 i 個揹包的最優解,      
                            // 要麼是不包含第 i 個揹包的最優解, 取兩者最大值,這裡採用了分類討論法      
                            // 第 i 個揹包的重量 iweight 和價值 ivalue      
                            int iweight = bags[i-1].getWeight();      
                            int ivalue = bags[i-1].getValue();      
                            bestValues[i][j] =       
                                Math.max(bestValues[i-1][j], ivalue + bestValues[i-1][j-iweight]);            
                        } // else      
                    } //else               
               } //for      
            } //for      
                  
            // 求解揹包組成      
            if (bestSolution == null) {      
                bestSolution = new ArrayList<Knapsack>();      
            }      
            int tempWeight = totalWeight;      
            for (int i=n; i >= 1; i--) {      
               if (bestValues[i][tempWeight] > bestValues[i-1][tempWeight]) {      
                   bestSolution.add(bags[i-1]);  // bags[i-1] 表示第 i 個揹包      
                   tempWeight -= bags[i-1].getWeight();      
               }      
               if (tempWeight == 0) { break; }      
            }      
            bestValue = bestValues[n][totalWeight];      
        }      
              
        /**   
         * 獲得前  n 個揹包, 總承重為 totalWeight 的揹包問題的最優解值   
         * 呼叫條件: 必須先呼叫 solve 方法   
         *    
         */      
        public int getBestValue() {       
            return bestValue;      
        }      
              
        /**   
         * 獲得前  n 個揹包, 總承重為 totalWeight 的揹包問題的最優解值矩陣   
         * 呼叫條件: 必須先呼叫 solve 方法   
         *    
         */      
        public int[][] getBestValues() {      
                  
            return bestValues;      
        }      
              
        /**   
         * 獲得前  n 個揹包, 總承重為 totalWeight 的揹包問題的最優解值矩陣   
         * 呼叫條件: 必須先呼叫 solve 方法   
         *    
         */      
        public ArrayList<Knapsack> getBestSolution() {      
            return bestSolution;      
        }      
              
    }     
      
    public class KnapsackTest {      
            
        public static void main(String[] args) {      
                  
            Knapsack[] bags = new Knapsack[] {      
                    new Knapsack(2,13), new Knapsack(1,10),      
                    new Knapsack(3,24), new Knapsack(2,15),      
                    new Knapsack(4,28), new Knapsack(5,33),      
                    new Knapsack(3,20), new Knapsack(1, 8)      
            };      
                
            int totalWeight = 10;      
            KnapsackProblem kp = new KnapsackProblem(bags, totalWeight);      
                  
            kp.solve();      
            System.out.println(" -------- 該揹包問題例項的解: --------- ");      
            System.out.println("最優值:" + kp.getBestValue());       
            System.out.println("最優解【選取的揹包】: ");      
            System.out.println(kp.getBestSolution());      
            System.out.println("最優決策矩陣表:");      
            int[][] bestValues = kp.getBestValues();      
            for (int i=0; i < bestValues.length; i++) {      
                for (int j=0; j < bestValues[i].length; j++) {      
                    System.out.printf("%-5d", bestValues[i][j]);      
                }      
                System.out.println();      
            }      
        }      
    }