一文學懂動態規劃

前言

動態規劃(dynamic programming,簡稱 dp)是工程中非常重要的解決問題的思想,從我們在工程中地圖軟件上應用的最短路徑問題,再在生活中的在淘寶上如何湊單以便利用滿減券來最大程度地達到我們合理薅羊毛的目的 ,很多時候都能看到它的身影。不過動態規劃對初學者來說確實比較難,dp狀態,狀態轉移方程讓人摸不着頭腦,網上很多人也反饋不太好學,其實就像我們之前學過的遞歸一樣,任何算法的學習都是有它的規律和套路的,只要掌握好它的規律及解題的套路,再加上大量的習題練習,相信掌握它不是什麼難事,本文將會用比較淺顯易懂地講解來幫助大家掌握動態規劃這一在工程中非常重要的思想,相信看完後,動態規劃的解題套路一定能手到擒來(文章有點長,建議先收藏再看,看完後一定會對動態規劃的認知上升到一個臺階!)

本文將會從以下角度來講解動態規劃:

 

  • 什麼是動態規劃
  • 動態規劃從入門到進階
  • 再談動態規劃

什麼是動態規劃

以下是我綜合了動態規劃的特點給出的動態規劃的定義:動態規劃是一種多階段決策最優解模型,一般用來求最值問題,多數情況下它可以採用自下而上的遞推方式來得出每個子問題的最優解(即最優子結構),進而自然而然地得出依賴子問題的原問題的最優解。

  1. 多階段決策,意味着問題可以分解成子問題,子子問題,......,也就是說問題可以拆分成多個子問題進行求解
  2. 最優子結構,在自下而上的遞推過程中,我們求得的每個子問題一定是全局最優解,既然它分解的子問題是全局最優解,那麼依賴於它們解的原問題自然也是全局最優解。
  3. 自下而上,怎樣才能自下而上的求出每個子問題的最優解呢,可以肯定子問題之間是有一定聯繫的,即迭代遞推公式,也叫「狀態轉移方程」,要定義好這個狀態轉移方程, 我們就需要定義好每個子問題的狀態(DP 狀態),那爲啥要自下而上地求解呢,因爲如果採用像遞歸這樣自頂向下的求解方式,子問題之間可能存在大量的重疊,大量地重疊子問題意味着大量地重複計算,這樣時間複雜度很可能呈指數級上升(在下文中我們會看到多個這樣重複的計算導致的指數級的時間複雜度),所以自下而上的求解方式可以消除重疊子問題。

簡單總結一下,最優子結構,狀態轉移方程,重疊子問題就是動態規劃的三要素,這其中定義子問題的狀態與寫出狀態轉移方程是解決動態規劃最爲關鍵的步驟,狀態轉移方程如果定義好了,解決動態規劃就基本不是問題了。

既然我們知道動態規劃的基本概念及特徵,那麼怎麼判斷題目是否可以用動態規劃求解呢,其實也很簡單,當問題的定義是求最值問題,且問題可以採用遞歸的方式,並且遞歸的過程中有大量重複子問題的時候,基本可以斷定問題可以用動態規劃求解,於是我們得出了求解動態規劃基本思路如下(解題四步曲)

  1. 判斷是否可用遞歸來解,可以的話進入步驟 2
  2. 分析在遞歸的過程中是否存在大量的重複子問題
  3. 採用備忘錄的方式來存子問題的解以避免大量的重複計算(剪枝)
  4. 改用自底向上的方式來遞推,即動態規劃

可能不少人看了以上的動態規劃的一些介紹還是對一些定義如 DP 狀態,狀態轉移方程,自底而上不了解,沒關係 ,接下來我們會做幾道習題來強化一下大家對這些概念及動態規劃解題四步曲的理解,每道題我們都會分別用遞歸,遞歸+備忘錄,動態規劃來求解一遍,這樣也進一步幫助大家來鞏固我們之前學的遞歸知識

動態規劃從入門到進階

 

入門題:斐波那契數列

接下來我們來看看怎麼用動態規劃解題四步曲來解斐波那契數列

注:斐波那契數列並不是嚴格意義上的動態規劃,因爲它不涉及到求最值,用這個例子旨在說明重疊子問題與狀態轉移方程

1、判斷是否可用遞歸來解 顯然是可以的,遞歸代碼如下

int fibonacci(int n) {
    if (n == 0||n==1) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

 

2、分析在遞歸的過程中是否存在大量的重複子問題

怎麼分析是否有重複子問題,畫出遞歸樹

可以看到光是求 f(6),就有兩次重複的計算, f(4) 求解了兩次,f(3) 求解了兩次,時間複雜度是指數級別,遞歸時間複雜度怎麼看,解決每個子問題需要的時間乘以子問題總數,每個子問題需要的時間即 f(n) = f(n-1) + f(n-2) 只做了一次加法運算,子問題的個數有多少呢,每個問題一分爲二,是個二叉樹,可以看到第一層 1 個,第二層 2 個,第三層 4 個,即 1 + 2 + 2^2 + .... 2^n,所以總的來說時間複雜度是)O(2^n),是指數級別

注:自頂向下: 這種從 原問題展開子問題進行求解的方式

3、採用備忘錄的方式來存子問題的解以避免大量的重複計算 既然以上中間子問題中存在着大量的重複計算,那麼我們可以把這些中間結果給緩存住(可以用哈希表緩存),如下

vector<int> m(n+1,0);
int fibonacci(int n) {
    if (n ==0||n==1) return 1;
    if (m[n]) 
        return m[n];
    return m[n] = fibonacci(n - 1) + fibonacci(n - 2);
}

 

這麼緩存之後再看我們的遞歸樹

 

可以看到通過緩存中間的數據,做了大量地剪枝的工作,同樣的f(4),f(3),f(2),都只算一遍了,省去了大量的重複計算,問題的規模從二叉樹變成了單鏈表(即 n),時間複雜度變成了 O(n),不過由於哈希表緩存了所有的子問題的結果,空間複雜度是 O(n)。

4、改用自底向上的方式來遞推,即動態規劃 我們注意到如下規律:

只要依次自底向上求出 f(3),f(4),...,自然而然地就求出了 f(n)

 注:自底向上:從最終地不能再分解的子問題根據遞推方程(f(n) = f(n-1) + f(n-2))逐漸求它上層的問題,上上層問題,最終求得一開始的問題

 

f(n) 就是定義的每個子問題的狀態(DP 狀態),f(n) = f(n-1) + f(n-2) 就是狀態轉移方程,即 f(n) 由 f(n-1), f(n-2) 這兩個狀態轉移而來,由於每個子問題只與它前面的兩個狀態,所以我們只要定義三個變量,自底向上不斷循環迭代即可,如下

int f(int n) {
    if (n == 0||n==1)
       return 1;
    if(n == 2)
       return 2;
    int result = 0;
    int pre = 1;
    int next = 2;
  
    for (int i = 3; i < n + 1; ++i) {
        result = pre + next;
        pre = next;
        next = result;
    }
    return result;
}

 這樣時間複雜度雖然還是O(n),但空間複雜度只由於只定義了三個變量(result,pre,next)所以是常量 O(1)。

通過簡單地斐波那契的例子,相信大家對自底向上,DP 狀態, DP 轉移方程應該有了比較深入地認識,細心的同學一定發現了最優子結構怎麼沒有,因爲前面我們也說了,斐波那契數列並不是嚴格意義上的動態規劃,只是先用這個簡單地例子來幫助大家瞭解一下一些基本的概念。在之後的習題中我們將會見識到真正的動態規劃。

經典入門:三角形的最小路徑和

 如圖示,以上三角形由一連串的數字構成,要求從頂點 2 開始走到最底下邊的最短路徑,每次只能向當前節點下面的兩個節點走,如 3 可以向 6 或 5 走,不能直接走到 7。

 

如圖示:從 2 走到最底下最短路徑爲  2+3+5+1 = 11,即爲我們所求的

首先我們需要用一個二維數組來表示這個三個角形的節點,用二維數組顯然可以做到, 第一行的 2 用 a[0][0] 表示,第二行元素 3, 4 用 a[1][0],a[1][1],依此類推。

定義好數據結構之後,接下來我們來看看如何套用我們的動態規劃解題套路來解題

1、 判斷是否可用遞歸來解

如果用遞歸,就要窮舉所有的路徑和,最後再求所有路徑和的最小值,我們來看看用遞歸怎麼做。

對於每個節點都可以走它的左或右節點,假設我們定義 traverse(i, j) 爲節點 a[i][j] 下一步要走的節點,則可以得出遞歸公式的僞代碼如下


traverse(i, j) =min {
    traverse(i+1, j);    向節點i,j 下面的左節點走一步
    traverse(i+1, j+1);    向節點i,j 下面的右節點走一步
}

對於每個節點,要麼向左或向右,每個問題都分解成了兩個子問題,和斐波那契數列一樣,如果畫出遞歸樹也是個二叉樹,所以時間複雜度是 O(2^n),也是指數級別。

2、分析在遞歸的過程中是否存在大量的重複子問題

爲啥時間複雜度是指數級別呢,我們簡單分析一下:

 對於節點 3 和 4 來說,如果節點 3 往右遍歷, 節點 4 往左遍歷,都到了節點 5,節點 5 往下遍歷的話就會遍歷兩次,所以此時就會出現重複子問題

3、改用自底向上的方式來遞推,即動態規劃

重點來了,如何採用自底向上的動態規劃來解決問題呢? 我們這麼來看,要求節點 2 到底部邊的最短路徑,只要先求得節點 3 和 節點 4 到底部的最短路徑值,然後取這兩者之中的最小值再加 2 不就是從 2 到底部的最短路徑了嗎,同理,要求節點 3 或 節點 4 到底部的最小值,只要求它們的左右節點到底部的最短路徑再取兩者的最小值再加節點本身的值(3 或 4)即可。

我們知道對於三角形的最後一層節點,它們到底部的最短路徑就是其本身,於是問題轉化爲了已知最後一層節點的最小值怎麼求倒數第二層到最開始的節點到底部的最小值了。先看倒數第二層到底部的最短路徑怎麼求

同理,第二層對於節點 3 ,它到最底層的最短路徑轉化爲了 3 到 7, 6 節點的最短路徑的最小值,即 9, 對於節點 4,它到最底層的最短路徑轉化爲了 4 到 6, 10 的最短路徑兩者的最小值,即 10。

 接下來要求 2 到底部的路徑就很簡單了,只要求 2 到節點 9 與 10 的最短路徑即可,顯然爲 11。

於是最終的 11 即爲我們所求的值,接下來我們來看看怎麼定義 DP 的狀態與狀態轉移方程。 我們要求每個節點到底部的最短路徑,於是 DP 狀態 DP[i,j] 定義爲 i,j 的節點到底部的最小值,DP狀態轉移方程定義如下:

DP[i,j] = min(DP[i+1, j], D[i+1, j+1]) + triangle[i,j]

 這個狀態轉移方程代表要求節點到最底部節點的最短路徑只需要求左右兩個節點到最底部的最短路徑兩者的最小值再加此節點本身!

DP 狀態 DP[i,j] 有兩個變量,需要分別從下而上,從左到右循環求出所有的 i,j, 有了狀態轉移方程求出代碼就比較簡單了,如下

int traverse(vector<vector<int>> triangle) {
    int ROW=triangle.size();
    for (int i = ROW - 2; i >= 0; --i) {
        for (int j = 0; j < triangle[j].size();++j) {
             triangle[i][j] += min(triangle[i+1][j],triangle[i+1][j+1]);
        }
    }
    return triangle[0][0];
}

我們再來談談最優子結構,在以上的推導中我們知道每一層節點到底部的最短路徑依賴於它下層的左右節點的最短路徑,求得的下層兩個節點的最短路徑對於依賴於它們的節點來說就是最優子結構,最優子結構對於子問題來說屬於全局最優解,這樣我們不必去求節點到最底層的所有路徑了,只需要依賴於它的最優子結構即可推導出我們所要求的最優解,所以最優子結構有兩層含義,一是它是子問題的全局最優解,依賴於它的上層問題只要根據已求得的最優子結構推導求解即可得全局最優解,二是它有緩存的含義,這樣就避免了多個依賴於它的問題的重複求解(消除重疊子問題)。

總結:仔細回想一下我們的解題思路,我們先看了本題是否可用遞t歸來解,在遞歸的過程中發現了有重疊子問題,於是我們又用備忘錄來消除遞歸中的重疊子問題,既然我們發現了此問題可以用遞歸+備忘錄來求解,自然而然地想到它可以用自底向上的動態規劃來求解。是的,求解動態規劃就按這個套路來即可,最重要的是要找出它的狀態轉移方程,這需要在自下而上的推導中仔細觀察。

 

進階:湊零錢

給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。輸入: coins = [1, 2, 5], amount = 11,輸出: 3  解釋: 11 = 5 + 5 + 1 輸入: coins = [2], amount = 3,輸出: -1
由以上的四步曲我們可以得出其狀態轉移方程如下:

DP[i] =  min{ DP[ i - coins[j] ] + 1 } = min{ DP[ i - coins[j] ]} + 1,  其中 j 的取值爲 0 到 coins 的大小,i 代表取了 coins[j] 這一枚硬幣。

由此可以寫出如下動態規劃代碼:

int exchangeDP(int amount, vector<int> coins) {
    vector<int> dp(amount+1,0);
    // 初始化每個值爲 amount+1,這樣當最終求得的 dp[amount] 爲 amount+1 時,說明問題無解
    for (int i = 0; i < amount + 1; i++) {
        dp[i] = amount + 1;
    }

    // 0 硬幣本來就沒有,所以設置成 0
    dp[0] = 0;
    for (int i = 0; i < amount + 1; ++i) 
        for (int j = 0; j < coins.size(); ++j) 
            if (i >= coins[j])
                dp[i] = min(dp[i- coins[j]], dp[i]) + 1;

    if (dp[amount] == amount + 1) 
        return -1;
    return dp[amount];
}

 

湊零錢這道題還可以用另外一道經典的青蛙跳臺階的思路來考慮,從最底部最少跳多少步可以跳到第 11 階,一次可以跳 1,2,5步 。由此可知最後一步一定是跳 1 或 2 或 5 步,於是如果用 f(n) 代表跳臺階 n 的最小跳數,則問題轉化爲了求 f(n-1),f(n-2) ,f(n-5)的最小值。

 如圖示:最後一跳一定是跳 1 或 2 或 5 步,只要求  f(n-1),f(n-2) ,f(n-5)的最小值即可


寫出遞推表達式, 即:

 f(n) = min{ f(n-1),f(n-2),f(n-5)} + 1 (1代表最後一跳)

我們的 DP 狀態轉移方程對比一下,可以發現兩者其實是等價的,只不過這種跳臺階的方式可能更容易理解。

總結

本文通過幾個簡單的例子強化了大家動態規劃的三要素:最優子結構,狀態轉移方程,重疊子問題的理解,相信大家對動態規劃的理解應該深刻了許多,怎麼看出是否可以用動態規劃來解呢,先看題目是否可以用遞歸來推導,在用遞歸推導的過程如果發現有大量地重疊子問題,則有兩種方式可以優化,一種是遞歸 + 備忘錄,另一種就是採用動態規劃了,動態規劃一般是自下而上的, 通過狀態轉移方程自下而上的得出每個子問題的最優解(即最優子結構),最優子結構其實也是窮舉了所有的情況得出的最優解,得出每個子問題的最優解後,也就是每個最優解其實是這個子問題的全局最優解,這樣依賴於它的上層問題根據狀態轉移方程自然而然地得出了全局最優解。動態規劃自下而上的求解方式還有一個好處就是避免了重疊子問題,因爲依賴於子問題的上層問題可能有很多,如果採用自頂而下的方式來求解,就有可能造成大量的重疊子問題,時間複雜度會急劇上升。

更多動態規劃題:

九大揹包問題:可見揹包九講,絕對牛

最長公共子序列

最長不下降子序列

買賣股票的最佳時機

以及poj上:

1015 Jury Compromise 
1029 False coin 
1036 Gangsters 
1037 A decorative fence 
1038 Bugs Integrated, Inc. 
1042 Gone Fishing 
1050 To the Max 
1062 昂貴的聘禮 
1074 Parallel Expectations 
1080 Human Gene Functions 
1088 滑雪 
1093 Formatting Text 
1112 Team Them Up! 
1141 Brackets Sequence 
1143 Number Game 
1157 LITTLE SHOP OF FLOWERS 
1159 Palindrome 
1160 Post Office 
1163 The Triangle 
1170 Shopping Offers 
1178 Camelot 
1179 Polygon 
1180 Batch Scheduling 
1185 炮兵陣地 
1187 隕石的祕密 
1189 釘子和小球 
1191 棋盤分割 
1192 最優連通子集 
1208 The Blocks Problem 
1239 Increasing Sequences 
1240 Pre-Post-erous! 
1276 Cash Machine 
1293 Duty Free Shop 
1322 Chocolate 
1323 Game Prediction 
1338 Ugly Numbers 
1390 Blocks 
1414 Life Line 
1432 Decoding Morse Sequences 
1456 Supermarket 
1458 Common Subsequence 
1475 Pushing Boxes 
1485 Fast Food 
1505 Copying Books 
1513 Scheduling Lectures 
1579 Function Run Fun 
1609 Tiling Up Blocks 
1631 Bridging signals 2分+DP NLOGN 
1633 Gladiators 
1635 Subway tree systems 
1636 Prison rearrangement 
1644 To Bet or Not To Bet 
1649 Market Place 
1651 Multiplication Puzzle 
1655 Balancing Act 
1661 Help Jimmy 
1664 放蘋果 
1671 Rhyme Schemes 
1682 Clans on the Three Gorges 
1690 (Your)((Term)((Project))) 
1691 Painting A Board 
1692 Crossed Matchings 
1695 Magazine Delivery 
1699 Best Sequence 
1704 Georgia and Bob 
1707 Sum of powers 
1712 Flying Stars 
1714 The Cave 
1717 Dominoes 
1718 River Crossing 
1722 SUBTRACT 
1726 Tango Tango Insurrection 
1732 Phone numbers 
1733 Parity game 
1737 Connected Graph 
1740 A New Stone Game 
1742 Coins P 
1745 Divisibility 
1770 Special Experiment 
1771 Elevator Stopping Plan 
1776 Task Sequences 
1821 Fence 
1837 Balance 
1848 Tree 
1850 Code 
1853 Cat 
1874 Trade on Verweggistan 
1887 Testing the CATCHER 
1889 Package Pricing 
1920 Towers of Hanoi 
1926 Pollution 
1934 Trip 
1936 All in All 
1937 Balanced Food 
1946 Cow Cycling 
1947 Rebuilding Roads 
1949 Chores 
1952 BUY LOW, BUY LOWER 
1953 World Cup Noise 
1958 Strange Towers of Hanoi 
1959 Darts 
1962 Corporative Network 
1964 City Game 
1975 Median Weight Bead 
1989 The Cow Lineup 
2018 Best Cow Fences 
2019 Cornfields 
2029 Get Many Persimmon Trees 
2033 Alphacode 
2039 To and Fro 
2047 Concert Hall Scheduling 
2063 Investment 
2081 Recaman's Sequence 
2082 Terrible Sets 
2084 Game of Connections 
2127 Greatest Common Increasing Subsequence 
2138 Travel Games 
2151 Check the difficulty of problems 
2152 Fire 
2161 Chandelier 
2176 Folding 
2178 Heroes Of Might And Magic 
2181 Jumping Cows 
2184 Cow Exhibition 
2192 Zipper 
2193 Lenny's Lucky Lotto Lists 
2228 Naptime 
2231 Moo Volume 
2279 Mr. Young's Picture Permutations 
2287 Tian Ji -- The Horse Racing 
2288 Islands and Bridges 
2292 Optimal Keypad 
2329 Nearest number - 2 
2336 Ferry Loading II 
2342 Anniversary party 
2346 Lucky tickets 
2353 Ministry 
2355 Railway tickets 
2356 Find a multiple 
2374 Fence Obstacle Course 
2378 Tree Cutting 
2384 Harder Sokoban Problem 
2385 Apple Catching 
2386 Lake Counting 
2392 Space Elevator 
2397 Spiderman 
2411 Mondriaan's Dream 
2414 Phylogenetic Trees Inherited 
2424 Flo's Restaurant 
2430 Lazy Cows 
2915 Zuma 
3017 Cut the Sequence 
3028 Shoot-out 
3124 The Bookcase 
3133 Manhattan Wiring 
3345 Bribing FIPA 
3375 Network Connection 
3420 Quad Tiling ?

 

 

 

 

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