動態規劃定義
任何數學遞推公式都可以直接轉換成遞推算法,但是編譯器常常不能正確對待遞歸算法。將遞歸重新寫成非遞歸算法,讓後者把些子問題的答案系統地記錄在一個表內。利用這種方法的一種技巧叫做動態規劃
注:由已知推未知就是遞推,由未知推未知就是遞歸,這裏說的數學遞推公式有別與遞推算法。具體解釋如下: 如果數列{an}的第n項與它前一項或幾項的關係可以用一個式子來表示,那麼這個公式叫做這個數列的遞推公式。
爲什麼編譯器常常不能正確對待遞歸?
遞歸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];
}
總結
- 編譯器一般不能很好的處理遞歸,尤其是違反合成效益法則的遞歸
- 動態規劃需要分析,其重點在於以適用的數據結構保持遞歸步驟中的中間值。
- 是否需要將遞歸轉換爲非遞歸需要以實際項目的情況,酌情考慮。