動態規劃講義

漆楚衡發表於2014-11-30

動態規劃講義

  • 我們的理想是大草原。
  • At 2014.11.30 By 漆楚衡

基礎概念

  1. 資料結構
    • 對於一個複雜問題,其資料間往往有特殊的和規律的聯絡
    • 資料結構就是對資料的組織,意圖是在思維空間和計算空間表達這種聯絡
  2. 演算法
    • 對問題給出答案(以我們期望的表達形式)的過程描述,需要以下4條性質:
    • 輸入,輸出
    • 正確性
    • 有窮(這是演算法與程式的本質不同,程式不要求有窮)

資料結構與演算法為什麼總在一起?

資料結構是對複雜資料的組織,而演算法是資料的處理過程,也是資料結構的處理過程。所以對於經典的公式:

程式 = 演算法 + 資料

當資料變得複雜,需要組織的時候,也就需要寫成:

程式 = 演算法 + 資料結構 * 資料

相似而又不同(底層實現不同)的資料結構往往限制或優化了演算法的效率

E.g.

  • 二分法可以在陣列上取得O(logn)的複雜度,但在連結串列上就不行了
  • 而連結串列可以實現快速的插入刪除,陣列則不行

演算法也可以基於不同抽象層(高層邏輯不同)的資料結構,實現不同的邏輯功能

E.g.

  • 對於圖,有最短路問題,此時最短路演算法是基於圖的。跟底層實現無關。實際上,圖可以由線性表實現,從線性表的角度看,則完全不能表現最短路演算法在幹什麼。此時,最短路演算法是構建在圖這一邏輯結構基礎上的,而不是更底層的線性表。

面向資料結構的廣義演算法策略:分治

因為資料結構是資料的組織,我們稱資料結構所組織的基本單元為元素。當我們構建面向資料結構的演算法,我們實際上是構建面向資料結構的特性(元素間具有怎樣的關係)的演算法,演算法的主要處理物件是元素間的關係。

E.g.

  • 二叉樹上定義有層序遍歷演算法,也就是按照二叉樹所擁有的父子關係構建一條儲存所有元素的線性表,不關心表中元素
  • 線性表上的插入操作新增了元素,同時也新增了關係(線性表上的前驅與後繼關係),插入演算法只是操作關係,並不關心元素

一般來說,一個特定的據結構只用到一種(或幾種)特定的關係,否則便不可能具有可伸縮性。

E.g.

  • 線性表上有前驅與後繼(實際上是有向箭頭的兩端)關係
  • 二叉樹上有父子的關係

也就是關係的性質定義了資料結構,關係的數量定義了資料結構例項的規模。

面向資料結構的演算法實際上就是面向關係集合的演算法,那麼對於一個集合,我們要如何處理呢?

對於一個資料結構的例項a(比如說一條線性表),在到達底線前(底線一般是關係集合為空),我們都可以取出一個(或幾個)關係r,並得到一個較小的資料結構例項a',問題就變成了處理三個子問題:如何處理分離出來了的關係r和如何處理較小的集合a',以及如何將處理過的兩個子問題合併為原問題的解。

  • 我們假設已經知道如何處理r和合並子問題,否則你首要面對的問題就不是設計面向資料結構的演算法,而是搞清楚你要幹什麼。
  • 而對於a',因為a'a是相似的(只在規模上有所不同),那麼,一般可以套用處理a的演算法來處理a',這也就是遞迴。當a'的規模最終歸於零(也就是空集)時,自然,也假設已經知道如何處理了。

上文所展現的就是處理資料結構的演算法的一般思路——分治法

分治法對問題的要求:

  1. 問題是可分解的
    • 問題可以分解為(在分治過程所關心的性質上)相互獨立的子問題
    • 通過子問題的解可以合併出原問題的解
  2. 子問題與原問題用同一種策略求解

分治的一般步驟:

  1. 分解問題得到子問題
  2. 遞迴地解決子問題
  3. 將子問題的解合併為原問題的解

注意:分治法並不僅僅適用於資料結構,這裡只是說它是資料結構的通用處理思想

分治法可以細分為:

  • 減治法:每次處理一個固定大小的子問題
  • 變治法:每次處理的子問題的大小不固定(一般是與原問題成比例)

遞迴問題類(RClass)

RClass:由所有可以用遞迴方法解決的問題的集合

RClass所具有的性質

  1. 結構性:可以通過分解問題為子問題的方法解決原問題
    • 即問題可以看作是複合結構
    • 子問題之間是相互獨立的(這是子問題與原問題相似的前提)
  2. 自相似:問題與子問題用同一種策略求解

以上兩條同時是分治法對問題的要求,所以可以看出來解決RClass的本質策略是分治法(再向上看看,你發現了什麼嗎?)

動態規劃問題類(DPClass)

DPClass:所有可以通過動態規劃(DP:Dynamic Programming)高效解決的問題的集合

DPClass是什麼?

  • DPClass是RClass的真子集,這一點體現在DPClass一般可以寫出一個遞推方程

  • 遞推方程就是對遞迴的一種描述。

E.g. 對於階乘,有

f(n) = 1            n = 0
f(n) = n * f(n - 1)    n > 0
  • DPClass中一般是最優化問題

    • 最優化問題也可以看成是搜尋最優解的問題
  • 當問題具有以下性質才能藉由DP方法高效解決

    1. 最優子結構(無後效性):對於最優化問題,當問題分解為子問題,並求出子問題的最優解時,合併子解的過程只需要子最優解
    2. 子問題重疊:因為子問題有重複,也就是說我們可以考慮如何消除重複而達到節約計算的目的。
    3. 1是動態規劃(在最優化問題中)起作用的要求,2是動態規劃的優化目標。

E.g. 斐波那契數列的遞迴計算中有大量子問題相同,因此可以通過DP方法作效率改進。

E.g. (反例)階乘雖然也有遞推方程,但是因為階乘既不具有子問題重疊性質,所以用DP不會有顯著效率改進。

RClass的方法解決DPClass

因為DPClass是RClass的子集,自然也可以用分治(遞迴)的方法來求解。

我們可以用樹來表示遞迴過程:整個樹就是整個遞迴計算過程,樹中的每一個子樹代表了一個子問題。

因為是樹,對於兩個子問題,如果兩者之間沒有祖先-子孫關係,則求值過程是完全不相關的。這也就表示完全無法利用到子問題重疊性質。

E.g. 用遞迴的方法求解斐波那契數,將整個遞迴過程畫成一棵樹,則會發現大量重複節點。

記憶化搜尋

對於剛才的問題,有一個直接有效的解決方案,記憶化搜尋。

記憶化搜尋的思想非常簡單:因為我們希望同樣的問題只計算一次,那麼顯然是要把問題和對應的答案記下來,好在後來再次遇到同樣問題時直接給出答案。

於是有如下模板程式碼所示的記憶化搜尋方案:

setOfAnswers;    //記憶表
Ans slove(Problem p){
    if(setOfAnswers.haveAns(p)){    //查表,如果已經計算過這個問題,則直接返回答案
        return setOfAnswers.getAns(p);
    }
    setOfSubProblem = divide(p);    //分割問題為子問題集合
    setOfSubAns; //子解集合
    foreach ( pi in setOfSubProblem ){    //處理每一個子問題
        setOfSubAns += slove(pi);
    }
    ans = merge(subOfSubAns);    //合併子解集得到解
    setOfAnswers += (p, ans);        //將問題的解加入記憶表
    return ans;
}

E.g. fibonacci的記憶化搜尋方法:

int ans[Max] = {0};
int fib(int n){
    if(ans[n] != 0){
        return ans[n];
    }
    if(n < 2){
        ans[n] = 1;
    } else {
        ans[n] = fib(n - 1) + fib(n - 2);
    }
    return ans[n];
}

有向無環圖(DAG)

DAG:對樹放寬限制,允許有一個節點有多個父節點,但是不允許一個節點指向其祖先節點,所以稱無環。

我們觀察記憶化搜尋方法,我們可以將其理解為一個將遞迴樹摺疊到一個DGA的方法:代表了相同問題的節點都被摺疊到一起,於是對於某個重複的子問題,會有多個父節點指向它。

動態規劃(DP)

不僅是重複計算,對於DPClass,其實遞迴都是不必要的。

消除遞迴,我們有兩條路

  • 用棧模擬遞迴,這一方法可以提高演算法的執行效率,而且並不需要改變太多,只要將遞迴改為進出棧。
  • 自底向上計算:先計算所有的葉子節點,再向上一層……這是一條更徹底的道路,當然並不是任何時候都行得通。
    • 自底向上有與廣度優先搜尋同樣的問題:要儲存至少一層的資訊,而樹形結構的層級節點增長速度可能非常誇張(指數爆炸)。
    • 自底向上也需要知道“底”在哪裡,這並不總是一件清晰明瞭的事。

DP即是使用自底向上計算DPClass中問題的方法。

對於上述的自底向上計算問題的兩條缺陷,在DP中表現的並不明顯

  • DPClass的“底層”一般比較清楚:因為比較規則的分治策略,“底層”一般比較整齊,而且可以預測。

  • DP所解決的問題的子問題一般有大量重疊。實際上重疊程度越高,DP的效果越好。如果重疊程度很低,那麼採用DP也沒有多大意義。

實際上從個人經驗來說,DP在演算法競賽中表現的非常規則:

  • 將原遞迴樹中的每一層(將對應DAG也看做分層圖)視作一個整體,可以用DP解決的問題每一層的規模一般要麼相同,要麼有規律變化。
  • 一層一層的逐級向上處理是DP的基本模式

就像迴圈可以看作線性表上遞迴的一種實現,遞迴也可以看做特殊遞迴問題的實現方法。

DP的一般思路:

  • 分析問題,建立遞推方程
  • 反推遞推方程構建自“底”向上的計算過程
    • 一般就是以遞推方程中的基礎項(非遞迴定義項)開始,逐級向“遠離”基礎項的方向計算

關於最優子結構

對於最優化問題,要求問題具有最優子結構才能用DP方法有效解決。因為對於最優化問題,DP的重要作用是減少從“低階”上傳到“高階”的解的數量,如果沒有最優子結構,那麼顯然我們要上傳所有的可能解,而不是一個最優解,此時空間效率將無法保證。

動態規劃 VS 記憶化搜尋

DP的優勢:

  • 消除了遞迴或棧空間,加速
  • 一般DP有規則的填表結構,對於記憶體管理非常友好,進一步加速

記憶化搜尋的優勢:

  • 簡單,只要直接解讀遞推方程即可
  • 因為是自頂向下計算,不會去計算用不到的問題分支

相關文章