動態規劃

初探動態規化

剛學動態規劃,或多或少都有一些困惑。今天我們來看看什麼是動態規劃,以及他的應用。
學過分治方法的人都知道,分治方法是通過組合子問題來求解原問題,而動態規劃與分治方法相似,都是通過組合子問題的解來求解原問題,只不過動態規劃可以解決子問題重疊的情況,即不同的子問題有公共的子子問題。
說了這麼多的比較苦澀的話,只是爲了回頭再看,我們通過一個例子來具體說明一下:

鋼條切割問題

小王剛來到一家公司,他的頂頭boss買了一條長度爲10的鋼條,boss讓小王將其切割爲短鋼條,使得這條鋼條的價值最大,小王應該如何做?我們假設切割工序本身沒有成本支出。
已知鋼條的價格表如下:

長度 i 1 2 3 4 5 6 7 8 9 10
價格P(i) 1 5 8 9 10 17 17 20 24 30

小王是一個非常聰明的人,立刻拿了張紙畫了一下當這根鋼條長度爲4的所有切割方案(將問題的規模縮小)
這裏寫圖片描述
小王很快看出了可以獲得的最大收益爲5+5=10,他想了想,自己畫圖以及計算最大值的整個流程,整理了一下:
1>先畫出切一刀所有的切割方案:(1+8)、(5+5)、(8+1)一共三種可能,也就是 4 - 1中可能,把這個 4 換成 n(將具體情況換成一般情況),就變成了長度爲 n 的鋼條切一刀會有 n -1 中可能,然後在將一刀切過的鋼條再進行切割。
同上,(1+8)這個組合會有 2 中切法,(1+1+5)和(1+5+1)【看圖】,同理,(5+5)會有兩種切法(1+1+5)和(5+1+1),由於(1+1+5)和上面的(1+1+5)重合,所以算一種切法,依次類推。
由於我們對 n-1個切點總是可以選擇切割或不切割,所以長度爲 n 的鋼條共有 2^(n-1)中不同的切割方案不懂的點我
2>從這2^(n-1)中方案中選出可以獲得最大收益的一種方案
學過遞歸的小王很快就把上述過程抽象成了一個函數,寫出了以下的數學表達式:
設鋼條長度爲n的鋼條可以獲得的最大收益爲 r(n) (n>=1)
這裏寫圖片描述
第一個參數P(n)表示不切割對應的方案,其他 n-1個參數對應着另外 n-1中方案(對應上面的一刀切)
爲了求解規模爲 n 的原問題,我們先求解形式完全一樣,但規模更小的子問題。即當完成首次切割後,我們將兩段鋼條看成兩個獨立的鋼條切割問題來對待。我們通過組合兩個相關子問題的最優解,並在所有可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解我們成鋼條問題滿足最優子結構性質
編程能力很強的小王拿出筆記本
很快的在電腦上寫下了如下代碼

#include <stdio.h>

int CUT_ROD(int * p ,int n);
int max(int q, int a);

int main(void){
    int i = 0;
    int p[10] = {1,5,8,9,10,17,17,20,24,30};
    printf("請輸入鋼條的長度(正整數):\n");
    scanf("%d",&i);

    int maxEarning = CUT_ROD(p,i); // 切割鋼條
    printf("鋼條長度爲 %d 的鋼條所能獲得的最大收益爲:%d\n",i,maxEarning);

    return 0;
}
// 切割鋼條
int CUT_ROD(int * p,int n){
    int i;
    if(n < 0){
        printf("您輸入的數據不合法!\n");
        return -1;
    }else if(n == 0){
        return 0;
    }else if(n > 10){
        printf("您輸入的值過大!\n");
        return -1;
    }
    int q = -1;
    for(i = 0; i < n;i++){
        q = max(q,p[i] + CUT_ROD(p,n-1-i));
    }
    return q;
}

int max(int q, int a){
    if(q > a)
        return q;
    return a;
}

沾沾自喜的小王拿着自己的代碼到boss面前,說已經搞定了。boss看了看他的代碼,微微一笑,說,你學過指數爆炸沒,你算算你的程序的時間複雜度是多少,看看還能不能進行優化?小王一聽蒙了,自己這些還沒有想過,自己拿着筆和紙算了好大一會,得出了複雜度爲T(n) = 2^n,沒想到自己寫的代碼這麼爛,規模稍微變大就不行了。boss看了看小王,說:你想想你的代碼效率爲什麼差?小王想了想,說道:“我的函數CUT-ROD反覆地利用相同的參數值對自身進行遞歸調用,它反覆求解了相同的子問題了”boss說:“還不錯嘛?知道問題出在哪裏了,那你怎樣解決呢?”小王搖了搖頭,boss說:“你應該聽過動態規劃吧,你可以用數組把你對子問題求解的值存起來,後面如果要求解相同的子問題,直接用之前的值就可以了,動態規劃方法是付出額外的內存空間來節省計算時間,是典型的時空權衡”,聽到這裏小王暗自佩服眼前的boss,薑還是老的辣呀。
boss說完拿起小王的筆記本,寫下了如下代碼:

// 帶備忘的自頂向下法 求解最優鋼條切割問題
#include <stdio.h>

int MEMOIZED_CUT_ROD(int * p,int n);
int MEMOIZED_CUT_ROD_AUX(int * p, int n, int * r);
int max(int q,int s);

int main(void){
    int n;
    int p[11]={-1,1,5,8,9,10,17,17,20,24,30};
    printf("請輸入鋼條的長度(正整數 < 10):\n");
    scanf("%d",&n);
    if(n < 0 || n >10){
        printf("您輸入的值有誤!");
    }else{
        int r = MEMOIZED_CUT_ROD(p,n);
        printf("長度爲%d的鋼條所能獲得的最大收益爲:%d\n",n,r);
    }
    return 0;
}

int MEMOIZED_CUT_ROD(int * p, int n){
    int r[20];
    for(int i = 0; i <= n; i++){
        r[i] = -1; 
    }
    return MEMOIZED_CUT_ROD_AUX(p,n,r); 
}

int MEMOIZED_CUT_ROD_AUX(int * p, int n, int * r){
    if(r[n] >= 0){
        return r[n];
    }
    if(n == 0){
        return 0;
    }else{
        int q = -1;
        for(int i = 1; i <= n; i++){// 切割鋼條, 大綱有 n 中方案
            q = max(q,p[i] + MEMOIZED_CUT_ROD_AUX(p,n-i,r));  
        }
        r[n] = q;// 備忘
        return q;
    }
}

int max(int q, int s){
    if(q > s){
        return q;
    }
    return s;
}

小王兩眼瞪的直直的。boss好厲害,我這剛入職的小白還得好好修煉呀。
寫完boss說:這中方法被稱爲帶備忘的自頂向下法。這個方法按自然的遞歸形式編寫過程,但過程會保存每個子問題的解(通常保存在一個數組或散列表中),當需要一個子問題的解時,過程首先檢查是否已經保存過此解,如果是,則直接返回保存的值。還有一種自底向上法。這種方法一般需要恰當定義子問題的規模,使得任何子問題的求解都只依賴於“更小的”子問題的求解。因而我們可以將子問題按規模排序,按由小至大的順序進行求解,當求解某個子問題是,它所依賴的那些更小的子問題都已經求解完畢,結果已經保存,每個子問題只需求解一次。如果你有興趣回去好好看看書自己下去好好研究下吧。
最後再考考你,我寫的這個帶備忘的自頂向下法的時間複雜度是多少?小王看了看代碼,又是循環,又是遞歸的,腦子都轉暈了。boss接着說:“你可以這樣想,我這個函數是不是對規模爲0,1,…,n的問題進行了求解,那麼你看,當我求解規模爲n的子問題時,for循環是不是迭代了n次,因爲在我整個n規模的體系中,每個子問題只求解一次,也就是說我for循環裏的遞歸直接返回的是之前已經計算了的值,比如說 求解 n =3的時候,for(int i = 1,i <=3;i++),循環體執行三次,n=4時,循環體執行四次,所以說,我這個函數MEMOIZED_CUT_ROD進行的所有遞歸調用執行此for循環的迭代次數是一個等差數列,其和是O(n^2)”,是不是效率高了許多。小王嗯嗯直點頭,想着回去得買本《算法導論》好好看看。

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