動態規劃實例解析

參考:https://www.zhihu.com/question/39948290

https://baijiahao.baidu.com/s?id=1635388976060265522&wfr=spider&for=pc

喬治·桑塔亞納說過,“那些遺忘過去的人註定要重蹈覆轍。”這句話放在問題求解過程中也同樣適用。不懂動態規劃的人會在解決過的問題上再次浪費時間,懂的人則會事半功倍。那麼什麼是動態規劃?這種算法有何神奇之處?本文作者給出了初步的解答。

假設你正在使用適當的輸入數據進行一些計算。你在每個實例中都進行了一些計算,以便得到一些結果。當你提供相同的輸入時,你不知道會有相同的輸出。這就像你在重新計算之前已經計算好的特定結果一樣。

那麼問題出在哪裏呢?你之前計算某些結果的寶貴時間被浪費掉了。你可以通過保存之前的計算結果去輕易地解決這個問題。比如通過使用恰當的數據結構。舉個例子,你可以將輸入輸出作爲鍵值對映射保存起來。

那些遺忘過去的人註定要重蹈覆轍 ~ 動態規劃

現在通過分析這個問題,我們可以將新的輸入(或者不在數據結構中的輸入)與其對應的輸出存儲下來。或者在字典中查找輸入並返回相應的輸出結果。這樣當你在進行一些計算時,你可以檢查數據結構中是否存在該輸入,如果數據輸入存在的話就可以直接獲得結果。我們將與這種方法相關的技巧稱作動態規劃。

詳解動態規劃

現在讓我們更詳細地介紹動態規劃。

簡而言之,我們可以說動態規劃主要用來解決一些希望找到問題最優解的優化問題。

一種可以用動態規劃解決的情況就是會有反覆出現的子問題,然後這些子問題還會包含更小的子問題。相比於不斷嘗試去解決這些反覆出現的子問題,動態規劃會嘗試一次解決更小的子問題。之後我們可以將結果輸出記錄在表格中,我們在之後的計算中可以把這些記錄作爲問題的原始解。

舉個例子,有n個階梯,一個人每一步只能跨一個臺階或是兩個臺階,問這個人一共有多少種走法?

 

以下是三種不同的動態規劃解決方案:

遞歸求解:原始遞歸求解

自上而下:你從最頂端開始不斷地分解問題,直到你看到問題已經分解到最小並已得到解決,之後只用返回保存的答案即可。這叫做記憶存儲(*Memoization*)。

自下而上:你可以直接開始解決較小的子問題,從而獲得最好的解決方案。在此過程中,你需要保證在解決問題之前先解決子問題。這可以稱爲表格填充算法(*Tabulation,*table-filling algorithm**)。至於迭代和遞歸與這兩種方法的關係,自下而上用到了迭代技術,而自上而下則用到了遞歸技術。

假設n=10,顯而易見從0到10的走法等於從1到10和從2到10走法的和,climb(0)=climb(1)+climb(2),也就是說climb(i)=climb(i+1)+climb(i+2),這就是這個問題的核心(好多文章都稱爲最優子結構)

我們先來個原始遞歸求解,來個遞歸求解

int climb(int x)
{

    if (x == 10)
        return 1;
    if (x > 10)
        return 0;

    int s = climb(x + 1);
    int t = climb(x + 2);

     return s + t;


}

int main()
{
   int q = climb(0);

}

我們可以發現原始遞歸求解會重複計算好多,大家不清楚的話最好畫個樹看看遞歸到底重複計算了多少,好多算法都和數據結構綁在一起,理解了樹的結構,對算法有比較大的幫助。

第二種方法自上而下記憶存儲求解:

int climbmaxR[10] = { -1 };
int climb(int x)
{

    if (x == 10)
        return 1;
    if (x > 10)
        return 0;

    if (climbmaxR[x] != -1)
       return climbmaxR[x];

    int s = climb(x + 1);
    int t = climb(x + 2);
     int k = s + t;
     climbmaxR[x] =k;

     return s + t;


}

int main()
{
    for (int i = 0; i < 10; ++i)
        climbmaxR[i] = -1;
    int q = climb(0);
    return 0;

}

最後一種動態規劃求解方法,自下而上求解。我們可以發現如果要求解climb(8),需要知道climb[9]和climb[10]。那我們可以先算出

climb[9]和climb[10]然後依次往上推即可。

int main()
{

    int f[11];
    f[10] = 1;
    f[9] = 1;

    for (int i = 8; i >= 0; --i)
        f[i] = f[i + 1] + f[i + 2];
    return 0;

}

追根溯源

Richard bellman 是這個概念的提出者。他在 20 世紀 50 年代中期爲蘭德公司工作時想到了這一點。選擇「dynamic programming」這個名字的原因是爲了隱藏他爲這項研究所做的數學工作。因爲他擔心他的老闆會反對或不喜歡任何類型的數學研究。

所以「programming」這個詞只是一個參考,以表明這是一種老式的計劃或調度方式,通常是通過逐漸填充表格(以動態方式而不是線性方式)而不是一次全部填入的方式進行。

 

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