1
小宇:閃客,我最近在研究動態規劃,但感受就是想不明白,你能不能給我講講呀?算法
閃客:沒問題,這個我擅長,你先說說提到動態規劃,你最早想到的是什麼?編程
小宇:就什麼子問題呀、狀態轉移方程呀亂七八糟的,哎呀不行不行,我一想到這些腦子又嗡嗡響了。app
閃客:你先別急,你先把全部的名詞都拋在腦後,聽我講。ide
小宇:好滴,你說吧。spa
閃客:小宇我問你,從 1 一直加到 100 等於多少?3d
1 + 2 + 3 + ... + 100 = ?code
小宇:5050!server
閃客:你這,怎麼不按套路出牌呀,你應該說不知道。blog
小宇:人家高斯早就算出來了,我還裝不知道,這也太假了吧。遞歸
全劇終...
2
閃客:好吧,那我再給你出一個題。
小宇:行,你說吧,這回我確定說不知道。
閃客:一個樓梯有 10 級臺階,你從下往上走,每跨一步只能向上邁 1 級或者 2 級臺階,請問一共有多少種走法?
小宇:額,這我真不知道了,我想一想哈。
![](http://static.javashuo.com/static/loading.gif)
小宇:不行了不行了,實在想不明白,想了後面的就忘了前面的。
閃客:你仍是陷入了窮舉的思想,你仔細想一想我給你出的第一個題,看看有沒有思路。
小宇:啊!原來是有關聯的呀。
閃客:對呀,我原本想說假如我告訴你 1+...+99 是多少,你是否是就直接能算出 1+...+100 的值了。
小宇:哦你這麼一提示我有點感受了!要想走到第 10 級臺階,要麼是先走到第 9 級,而後再邁一步 1 級臺階上去,要麼是先走到第 8 級,而後一次邁 2 級臺階上去。
![](http://static.javashuo.com/static/loading.gif)
閃客:太棒了!你找到感受了!接着往下說。
小宇:這樣的話,走到 10 級臺階的走法數,就等於走到 9 級臺階的走法數,加上走到 8 級臺階的走法數。
閃客:很好,那假如走到第 x 級臺階的走法數咱們定義爲 F(x),那你能把剛剛的描述公式化麼?
小宇:那太簡單了,公式就是:
F(10) = F(9) + F(8)
閃客:沒錯,並且不光是 10 級臺階如此,走到任何一級臺階的走法數,都符合這個邏輯,所以就能夠得出一個通用公式:
F(x) = F(x-1) + F(x-2)
小宇:嗯嗯,這樣計算 F(10),只須要知道 F(9) 和 F(8) 就能夠了,而計算 F(8),就只須要知道 F(7) 和 F(6) 就能夠了,依次類推。
閃客:沒錯,那你想一想看 F(2) 和 F(1) 怎麼計算?
小宇:簡單,仍是剛剛都邏輯被,想知道 F(2),只須要知道 F(1) 和 F(0),誒不對 F(0) 是什麼鬼?還有 F(1) 的計算須要知道 F(0) 和 F(-1),不行呀,這解釋不通了。
閃客:哈哈,別急,在這道題裏,若是隻邁到 1 級臺階,那一共就一種走法;若是隻邁到 2 級臺階,就只有兩種走法。能夠直接很直觀地得出,不必推導。
![](http://static.javashuo.com/static/loading.gif)
小宇:哦哦我懂了,這道題裏因爲每個遞推項都須要前兩項的支持,因此必須有最開頭的兩項做爲已知,就是你說的 F(1) = 1 和 F(2) = 2。
閃客:沒錯。
小宇:嗯嗯,感受這樣就推出所有結果了!我寫一下程序你看看。
閃客:先別急,因爲這道題是一道經典的動態規劃題,因此咱們以這道題爲例子來定義動態規劃的三要素,在本題中
F(x-1) 和 F(x-2) 被稱爲 F(x) 的最優子結構
F(x) = F(x-1) + F(x-2) 叫狀態轉移方程
F(1) = 1, F(2) = 2 是問題的邊界
以後作動態規劃問題,只要找好這三個要素就行了。
小宇:哇,昇華了誒,逼格瞬間高了很多呢。
閃客:先別說這些廢話了,那接下來你看看能不能寫出程序,計算出 F(10) 的結果,這纔是難點。
小宇:編程的話這彷佛是個遞歸問題,簡單!
int getWays(int n) { if (n == 1) { return 1; } if (n == 2) { return 2; } return getWays(n-1) + getWays(n-2); }
閃客:嗯不錯,這樣很簡潔,但複雜度過高了,是 O(2^n),具體你能夠以後想一想爲何。如今你看看能不能將複雜度下降。
小宇:我想一想看,計算 F(10) 時須要計算 F(9) 和 F(8),而在遞歸計算 F(9) 時要計算 F(8) 和 F(7),這樣 F(8) 在這裏重複計算了,浪費了時間。
![](http://static.javashuo.com/static/loading.gif)
閃客:沒錯,其實計算新一個階段的值,只須要一直將其前兩個階段的值保存起來,就能夠一直算到最終的結果了。好比定義兩個變量 a 和 b 用於存儲前兩個階段的值,在計算 F(3) 時。
計算 F(4) 時,F(1) 的值就不用保存了,a 和 b 依次替換新值。
依此類推,最終就算出了 F(10) 的值。
固然你也能夠把以前的值都保留,但這樣就增長了空間複雜度,看你的需求了。
小宇:好的,那這樣代碼也很好寫,就這樣。
int getWays2(int n) { if (n == 1) { return 1; } if (n == 2) { return 2; } int a = 1; int b = 2; int temp = 0; for (int i = 3; i <= n; i++) { temp = a + b; a = b; b = temp; } return temp; }
閃客:不錯,這就是這道題正確的動態規劃解法,並且時間複雜度是 O(N),空間複雜度是 O(1)
小宇:哇,這就是動態規劃呀,原來這麼簡單。
3
閃客:不錯,動態規劃理解起來不難,難在當須要考慮的因素,也就是變化的維度多起來的時候,有的人就會頭腦發矇,很差找遞推公式了,並且這也確實是個難點。
小宇:哦是嗎?
閃客:那固然,我再給你出一道題。
小宇:來吧兄弟。
閃客:咳咳,那你聽好了。
有一個揹包,能夠裝載重量爲 5kg 的物品。
有 4 個物品,他們的重量和價值以下。
![](http://static.javashuo.com/static/loading.gif)
那麼請問,在不得超過揹包的承重的狀況下,將哪些物品放入揹包,可使得總價值最大?
小宇:明白了,就是我用這個揹包最多能裝走多少錢的東西。
閃客:是的。
小宇:哎呀不行,我又陷入走樓梯時的遍歷思想了。
閃客:不要緊,這道題能想出遍歷思想,其實也不容易了,你能夠先說一下,找找感受。
小宇:嗯嗯,那就是每一個物品均可以有放入揹包和不放入揹包兩種選擇。
若是總重量超過了揹包承重,那就不算,或者說將價值記爲 0,而後將全部狀況中價值最大的那個做爲結果。
這樣的複雜度也很容易得出,就是 O(2^N)
閃客:沒錯,這個複雜度很高的算法你已經說的很明白了,那接下來你想一想看用動態規劃思想,能不能解決這個問題。
小宇:好的,你以前說過,動態規劃的三要素是最優子結構、狀態轉移方程和邊界
閃客:沒錯,以前的變量不多因此比較簡單,如今變量多了,定義就變得難了起來,咱們先來幾個定義方便描述。咱們將 4 個物品的重量和價值分別表示爲:w1,w2,w3,w4,v1,v2,v3,v4。
![](http://static.javashuo.com/static/loading.gif)
假如咱們用
F(W,i)
表示
用載重爲 W 的揹包,裝前 i 件物品的最大價值
那本題其實就是
用載重爲 5kg 的揹包,裝前 4 件物品的最大價值
其實就是求解
F(5,4)
你能找到狀態轉移方程麼?
小宇:我想一想,單看這個物品 4,有兩種可能:
第一種可能:若是選擇把它裝入揹包,那已經獲得了 6 元錢。
此時揹包剩餘載重爲 1kg(5kg-4kg),剩餘物品是除去物品 4 後的前 3 件物品。
那這部分能獲取到的最大價值,至關於
用一個載重爲 1kg 的揹包,裝前 3 件物品的最大價值
哇,那這部分就是
F(1,3)
閃客:哈哈,你這本身說着說着就說對啦!
小宇:因此最終,若是選擇將物品 4 放入揹包,這種狀況下,最大價值就等於兩者之和。
F(1, 3) + 6
![](http://static.javashuo.com/static/loading.gif)
閃客:太好了小宇,那另外一種狀況呢?
小宇:第二種可能:若是選擇不裝這個物品 4,那更簡單了,就直接等於用一個載重爲 5 的揹包裝前 3 件物品的價值。
F(5, 3)
![](http://static.javashuo.com/static/loading.gif)
閃客:沒錯,並且就只有這兩種狀況!因此你看看 F(5,4)是否能用這兩種狀況的值表示呢?
小宇:哈哈,很簡單,就等於這兩種狀況當中的最大值唄。
F(5,4) = max { F(1, 3) + 6,F(5, 3) }
閃客:太好了,如今狀態轉移方程出來了,此時咱們畫個表格。
![](http://static.javashuo.com/static/loading.gif)
咱們的目標就是要計算右下角那個值,即揹包載重 W = 5 時,選擇前 4 件物品放入揹包的最大價值 F(5,4)
小宇:哇這個表格好清晰呀,根據上面的公式
F(5,4) = max { F(1,3) + 6, F(5,3) }
那也就是說只要知道 F(1,3) 和 F(5,3) 的值就能夠了對吧?
![](http://static.javashuo.com/static/loading.gif)
閃客:沒錯,那你再看看 F(1,3) 怎麼計算?
小宇:好的,F(1,3) 此時揹包重量爲 1,若是選擇放第三件物品的話,誒?好像不行,第三件物品根本放不下呀!
閃客:是的,因此這種狀況就不必討論放第三件物品的狀況了,由於根本放不下,所以 F(1,3) 直接就等於 F(1,2),因此只須要知道 F(1,2) 便可。
![](http://static.javashuo.com/static/loading.gif)
同理 F(1,2) 也直接等於 F(1,1),由於在揹包重量爲 1 時第二件物品也放不下。
![](http://static.javashuo.com/static/loading.gif)
閃客:小宇你想一想看,那 F(1,1) 又等於什麼呢?
小宇:顯然嘛,如今只有一件物品能夠選了,那能放下固然就放咯,因此最大價值就是第一件物品的價值 3,即 F(1,1) = 3
閃客:沒錯,這樣咱們就找到了一個邊界值,小宇你想一想看還有哪些邊界值能夠直接得出?你寫在表格裏吧。
小宇:好的,首先第一列表示揹包重量爲 0 時的狀況,那顯然什麼都裝不了,就全都是 0 了。
![](http://static.javashuo.com/static/loading.gif)
而後第一行也比較好算,揹包重量 >= 1 時能夠放下第一件物品,因此最大價值都等於 3
![](http://static.javashuo.com/static/loading.gif)
閃客:很好,接下來,就依次把表格的全部項都填出來,天然就能夠算出 F(5,4) 啦。
![](http://static.javashuo.com/static/loading.gif)
小宇:哇塞,這樣看好清晰呀!
閃客:是呀,不過剛剛咱們用的都是具體的數字,那咱們試着把這個問題抽象化,用一個載重爲 W 的揹包,裝載 N 件物品,每件物品的重量和價值分別用 wi 和 vi 來表示,那剛剛的狀態轉移方程是什麼呢?
小宇:emm,剛剛 F(5,4) = max { F(1,3) + 6, F(5,3) },若是都用變量表示的話,就是
F(W,N) = max { F(W-wn, N-1) + vn,F(W, N-1) }
閃客:很好,這就是狀態轉移方程。
F(W-wn, N-1) 和 F(W, N-1) 就是 F(W,N) 的最優子結構。
而剛剛表格中的第一行和第一列,即 F(0,...) 和 F(...,1) 就是邊界值!
小宇:哇塞我愛你閃客!終於有點理解動態規劃的思想了呢!
4
閃客:別高興太早,雖然過程看着清晰了,但代碼寫起來仍是有難度的,你今天回去就把代碼試着實現一下吧。
小宇:好的,保證完成任務。
閃客:快到晚飯時間了,旁邊新開了家餃子館,要不要一塊去吃呀?
小宇:哦不了,晚上想利用晚飯時間再去消化消化動態規劃的知識,不是還得代碼實現呢麼,下次吧,
閃客:哦好吧~
後記
本文經過直觀演示 01 揹包問題的解題思路,簡單說明了動態規劃思想的算法核心。可能很多人以爲動態規劃難在理解,因此花不少時間在理解其思想上。但其實理解核心思想,這一篇文章就夠了,更多的是經過不斷作題,反過來幫助本身理解動態規劃的思想。因此但願讀者在讀完本文後,和小宇同樣,動手將其代碼實現,並找來其餘變種題目,繼續鞏固。