動態規劃算法-揹包問題 原

動態規劃定義

任何數學遞推公式都可以直接轉換成遞推算法,但是編譯器常常不能正確對待遞歸算法。將遞歸重新寫成非遞歸算法,讓後者把些子問題的答案系統地記錄在一個表內。利用這種方法的一種技巧叫做動態規劃

注:由已知推未知就是遞推,由未知推未知就是遞歸,這裏說的數學遞推公式有別與遞推算法。具體解釋如下: 如果數列{an}的第n項與它前一項或幾項的關係可以用一個式子來表示,那麼這個公式叫做這個數列的遞推公式。

爲什麼編譯器常常不能正確對待遞歸?

遞歸4條基本法則

  1. 基準情形。必須有某些基準情形,它無需遞歸就能解出。
  2. 不斷推進。對於那些遞歸求解的步驟,每一次遞歸調用都必須要使情況朝一種
  3. 設計法則。假設所有的遞歸調用都能運行。
  4. 合成效益法則(compound interest rule)。在求解一個問題的實例時,切勿在不同的遞歸調用中做重複的操作。    遞歸的4條法則中,效率低下的遞歸實現經常觸犯第4條法則,即合成效益法則,也是編譯器通常無法正確對待遞歸的原因。下面舉例說明。

以求斐波那契數爲例說明

問題說明

有通項公式 f(n)=f(n-1)+f(n-2); f(0)=f(1)=1;求任意n對應的f(n)  

注意:目前有的編譯器可以優化尾遞歸

遞歸解法及存在的問題

    /**
     * 遞歸實現違反合成效益法則
     * */
    public static int fib(int n){
        if(n<=1){
            return 1;
        }else{
            return fib(n-1)+fib(n-2);
        }
    }
```  

以求f6爲例,計算f6需要計算f5和f4,而算f5是有需要計算f4+f3,則必定有重複計算的部分。具體詳細見下圖,(下圖紫色部分都是多餘計算)

![image](https://github.com/floor07/DataStructuresAndAlgorithm/blob/master/image/chapter10/dynamicprograming/fibonacci.png?raw=true)  

### 分析

由於計算F(N)只需要知道F(N-1)和F(N-2),因此我們只需要保留最近算出的兩個斐波那契數,並從f(2)開始一直計算的f(n)即可。

### 代碼實現

/**      * 動態規劃版本,保證沒有多餘的計算,      * 以last 保存f(i-1)的值,nextToLast保存f(i-2)      * answer 保存f(i)的值      * */     public static int fibonacci(int n){         if(n<=1){             return 1;         }         int last=1;         int nextToLast=1;         int answer=1;         for(int i=2;i<=n;i++){             answer=last+nextToLast;             nextToLast=last;             last=answer;         }         return answer;     }


# 小試牛刀解揹包問題

## 問題說明

假定揹包的最大容量爲W,N件物品,每件物品都有自己的價值val和重量wt,將物品放入揹包中使得揹包內物品的總價值最大(val的和最大)。

## 分析

臨時揹包總價值=Max{選取當前項揹包總價值,不選取當前項揹包總價值},轉換爲數學公式爲:  

 選取當前項時, 臨時揹包總價值=val[item-1]+V[item-1][weight-wt[item-1]]   

 不選取當前項,臨時揹包總價值= V[item-1][weight]   

 V[item][weight]=Math.max (val[item-1]+V[item-1][weight-wt[item-1]], V[item-1][weight]);  ```

進過上步驟分析,我們僅需保留以item爲行,以權重weight爲列的二維數組即可。具體實現如下:

代碼實現(非自實現)

 /**
     * @param val 權重數組
     * @param wt  重量數組
     * @param W   總權重
     * @return    揹包中使得揹包內物品的總價值最大時的重量
     */
    public static int knapsack(int val[], int wt[], int W) {
        //物品數量總和
        int N = wt.length; 
 
        //創建一個二維數組
        //行最多存儲N個物品,列最多爲總權重W,下邊N+1和W+1是保證從1開始
        int[][] V = new int[N + 1][W + 1]; 
 
        
        //將行爲 0或者列爲0的值,都設置爲0
        for (int col = 0; col <= W; col++) {
            V[0][col] = 0;
        }
        for (int row = 0; row <= N; row++) {
            V[row][0] = 0;
        }
        //從1開始遍歷N個物品
        for (int item=1;item<=N;item++){
            //一行一行的填充數據
            for (int weight=1;weight<=W;weight++){
              
                if (wt[item-1]<=weight){
                    //選取(當前項值+之前項去掉當前項權重的值)與不取當前項的值得最大者
                    V[item][weight]=Math.max (val[item-1]+V[item-1][weight-wt[item-1]], V[item-1][weight]);
                }else {//不選取當前項,以之前項代替
                    V[item][weight]=V[item-1][weight];
                }
            }
 
        }
 
        //打印最終矩陣
        for (int[] rows : V) {
            for (int col : rows) {
                System.out.format("%5d", col);
            }
            System.out.println();
        }
        //返回結果
        return V[N][W];
    }

總結

  1. 編譯器一般不能很好的處理遞歸,尤其是違反合成效益法則的遞歸
  2. 動態規劃需要分析,其重點在於以適用的數據結構保持遞歸步驟中的中間值。
  3. 是否需要將遞歸轉換爲非遞歸需要以實際項目的情況,酌情考慮。

代碼地址

github地址 

求Fibonacci數 

動態規劃算法解揹包  

碼雲地址

求Fibonacci數    動態規劃算法解揹包

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