動態規劃演算法——鋼條切割問題
動態規劃是通過組合子問題的解來求解原問題。與分治方法不同的是,動態規劃應用於子問題重疊的情況,即不同的子問題具有公共的子子問題。在這種情況下,分治策略會重複的計算那些公共子問題。而動態規劃是對每個子子問題只求解一次,將其儲存在一個表格中,從而避免重複計算這些問題。
動態規劃通常用於求解最優化問題(optimization problem)。這類問題擁有多個解,我們希望從中計算出最優解。當然,有些問題可能會不止一個最優解,此時,我們只需按照需求或計算一個最優解,或計算出所有的最優解。
通常有4個步驟設計一個動態規劃演算法。
1. 刻畫一個最優解的結構特徵; 2. 遞迴的定義最優解的值; 3. 計算最優解的值,通常有兩種方法,自底向上和自頂向下。 4. 利用計算出的資訊構造一個最優解。
鋼條切割的例子
給定一個長度為n英寸的鋼條和一個價格表
假設切割長度與價格表如下所示
長度 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
價格 |
1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
因為在鋼條左端
- 4 = 4 , 收益
r - 4 = 1+3,收益為
r=9 - 4 = 2+2,收益為
r=10 - 4 = 3+1 ,收益為
r=9 - 4 = 1+1+2, 收益為
r=7 - 4 = 1+2+1, 收益為
r=7 - 4 = 2+1+1,收益為
r=7 - 4=1+1+1+1,收益為
r=4
很明顯,當4=2+2時,可以得到最高的收益。那麼,我們怎樣用形式化的語言來描述它呢?
- 我們先切割第一段,我們可以在第1個位置切割,即4=1+3,也可以在第2個位置切割,即4=2+2等等。
- 就得到1+3,2+2,3+1這些種情況。同時,還要考慮鋼條不切割的情況。
- 計算最大收益。因為我們的目的是求怎樣切割(或者乾脆不切割)會得到最大收益。那麼,最大收益
r= - 依次切割,直到得到最大收益。
假設一個最優解是要將鋼條切割成
將鋼條切割長度分別為
更一般的,對於
第一個引數
為了求解規模為
最優子結構:問題的最優解由相關子問題的最優解組合而成,而這些子問題都可以獨立求解。
使用動態規劃解決鋼條切割的問題
雖然我們可以用較少較容易理解的遞迴程式碼解出此問題,然而,遞迴會重複計算相同的子問題,導致程式的執行時間以指數級的速度增長,即時間複雜度為
動態規劃的思想:使用遞迴方法之所以效率這麼低,是因為它會重複計算相同的子問題。因此,動態規劃方法仔細安排求解順序,對每個子問題只求解一次,並將其結果儲存下來。如果再次需要子問題的解,只需查詢儲存的結果,而不必重新計算。因此,動態規劃方法時典型的時空權衡(time-memory trade-off)的例子。
動態規劃有兩種等價的實現方法:
帶備忘錄的自頂向下方法(top-down with memorization)
此方法仍然按照自然的遞迴形式編寫過程,但過程中會儲存每個子問題的解(通常是儲存在陣列或者散列表中),當需要一個子問題的解時,會先判斷是否儲存過此解。如果是,則直接返回儲存的值,從而節省了計算時間。否則,按照常規方式進行計算。
程式碼如下
public static int memorizedCutRod(int[] p, int n){
int result = 0;
//儲存已計算過的子問題的解的陣列
int res[] = new int[n+1];
for (int i = 0; i < res.length; i++) {
res[i] = -1;
}
result = memorizedCutRodAux(p, n, res);
return result;
}
public static int memorizedCutRodAux(int[] p, int n, int[] r){
//如果已經計算過該子問題的解,直接返回
if(r[n]>=0){
return r[n];
}
int q = -1;
if(n==0){
q = 0;
} else{
//r(n)=max(p[i]+r(n-i))
//p[i]表示切割成長度為i的鋼條的收益
//r(n-i)剩餘鋼條的最大收益值
for(int i = 1;i<=n;i++){
q = Math.max(q, p[i]+memorizedCutRodAux(p, n-i, r));
}
}
r[n] = q;
return q;
}
自底向上的方法(bottom-up method)
這種方法一般需要恰當定義子問題的規模的概念,使得任何子問題的求解都只依賴於更小的子問題的求解。因而,我們可以將子問題按照規模排序,按照有小到大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已經求解完畢,結果已經儲存。每個子問題只需要求解一次,當我們求解它時,它的所有前提子問題都已經求解完成。
public static int bottomUpCutRod(int[] p, int n){
//儲存子問題的結果,res[n]就是我們所需的最優解
int[] res = new int[n+1];
//依次求出規模為i = 1...n的子問題
for(int i=1; i<=n; i++){
int q = -1;
for(int j=1;j<=i;j++){
q = Math.max(q, p[j]+res[i-j]);
}
res[i] = q;
}
return res[n];
}
現在我們只是求出來收益的最大值,並沒有求得解本身,即給出切割後每段鋼條的長度。我們可以擴充套件動態規劃演算法,使之對每個子問題不僅儲存最優收益值,還儲存對應的切割方案。
程式碼如下
public static void extendedBottomUpCutRod(int[] p, int n){
int[] res = new int[n+1];
//用來儲存最優解的切割的鋼條的長度
int[] solve = new int[n+1];
res[0] = 0;
for (int i = 1; i <= n; i++) {
int q = -1;
for(int j = 1; j<=i;j++){
if(q<p[j]+res[i-j]){
q = p[j]+res[i-j];
solve[i] = j;
}
}
res[i] = q;
}
print(res[n], solve, n);
}
public static void print(int maxValue, int[] solve, int n) {
System.out.println(maxValue);
while(n>0){
System.out.print(solve[n]+", ");
n = n - solve[n];
}
}
測試程式碼
public static void main(String[] args) {
int[] p = {0,1,5,8,9,10,17,17,20,24,30};
int n = 9;
int result = memorizedCutRod(p, n);
System.out.println(result);
int result2 = bottomUpCutRod(p, n);
System.out.println(result);
extendedBottomUpCutRod(p, n);
}