動態規劃( D ynamic P rogramming ,因此咱們簡稱動態規劃爲 DP )是 運籌學 的一個分支,是求解決策過程(decision process) 最優化的數學方法。 20 世紀 50 年代初 美國 數學家R.E.Bellman 等人在研究多階段決策過程 (multistep decision process) 的優化問題時,提出了著名的最優化原理 (principle of optimality),把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法 —— 動態規劃。 1957 年出版了他的名著《 Dynamic Programming 》,這是該領域的第一本著做。ios
動態規劃算法一般基於一個遞推公式及一個或多個初始狀態。當前子問題的解將由上一次子問題的解推出。使用動態規劃來解題只須要多項式時間複雜度,所以它比回溯法、暴力法等要快許多。算法
說了這麼多術語,想必你們都很頭疼, 如今讓咱們經過一個例子來了解一下DP 的基本原理。數組
首先,咱們要找到某個狀態的最優解,而後在它的幫助下,找到下一個狀態的最優解。 這句話暫時理解不了不要緊,請看下面的例子 :post
若是咱們有面值爲1 元、 3 元和 5 元的硬幣若干枚,如何用最少的硬幣湊夠 11 元?優化
咱們憑直觀感受告訴本身,先選面值最大,所以最多選 2枚 5 元的硬幣,如今是 10 元了,還差一元,接下來咱們挑選第二大的 3 元硬幣,發現不行( 10+3=13 超了),所以咱們繼續選第三大的硬幣也就是 1元硬幣,選一個就能夠( 10+1=11 ),因此總共用了 3 枚硬幣湊夠了 11 元。這就是貪心法,每次選最大的。可是咱們將面值改成 2 元, 3 元和 5 元的硬幣,再用貪心法就不行了。爲何呢?按照貪心思路,咱們一樣先取 2 枚最大 5 元硬幣,如今 10 元了,還差一元,接下來選第二大的,發現不行,再選第三大的,仍是不行,這時用貪心方法永遠湊不出 11 元,可是你仔細看看,其實咱們能夠湊出 11 元的, 2 枚 3元硬幣和 1 枚五元硬幣就好了,這是人通過思考判斷出來了的,可是怎麼讓計算機算出來呢?這就要用動態規劃的思想:spa
首先咱們思考一個問題,如何用最少的硬幣湊夠i 元 (i<11) ?爲何要這麼問呢?兩個緣由: 1. 當咱們遇到一個大問題時,老是習慣把問題的規模變小,這樣便於分析討論。 2. 這個規模變小後的問題和原來的問題是同質的,除了規模變小,其它的都是同樣的,本質上它仍是同一個問題 ( 規模變小後的問題實際上是原問題的子問題 ) 。.net
好了,讓咱們從最小的i 開始吧。當 i=0 ,即咱們須要多少個硬幣來湊夠 0 元。因爲 1 , 3 , 5 都大於 0,即沒有比 0 小的幣值,所以湊夠 0 元咱們最少須要 0 個硬幣。 ( 這個分析很傻是否是?彆着急,這個思路有利於咱們理清動態規劃究竟在作些什麼。 ) 這時候咱們發現用一個標記來表示這句 「 湊夠 0 元咱們最少須要 0 個硬幣。 」 會比較方便,若是一直用純文字來表述,不出一下子你就會以爲很繞了。那麼,咱們用 d(i)=j 來表示湊夠 i 元最少須要 j 個硬幣。因而咱們已經獲得了 d(0)=0 ,表示湊夠 0 元最小須要 0 個硬幣。當 i=1 時,只有面值爲 1 元的硬幣可用,所以咱們拿起一個面值爲 1 的硬幣,接下來只須要湊夠 0 元便可,而這個是已經知道答案的,即 d(0)=0 。因此, d(1)=d(1-1)+1=d(0)+1=0+1=1 。當 i=2 時,仍然只有面值爲 1 的硬幣可用,因而我拿起一個面值爲 1 的硬幣,接下來我只須要再湊夠 2-1=1 元便可 ( 記得要用最小的硬幣數量 ) ,而這個答案也已經知道了。因此 d(2)=d(2-1)+1=d(1)+1=1+1=2 。一直到這裏,你均可能會以爲,好無聊,感受像作小學生的題目似的。由於咱們一直都只能操做面值爲 1 的硬幣!耐心點,讓咱們看看 i=3 時的狀況。當 i=3 時,咱們能用的硬幣就有兩種了: 1 元的和 3 元的 ( 5 元的仍然沒用,由於你須要湊的數目是 3 元! 5 元太多了親 ) 。既然能用的硬幣有兩種,我就有兩種方案。若是我拿了一個 1 元的硬幣,個人目標就變爲了:湊夠 3-1=2 元須要的最少硬幣數量。即 d(3)=d(3-1)+1=d(2)+1=2+1=3 。這個方案說的是,我拿 3 個 1 元的硬幣;第二種方案是我拿起一個 3 元的硬幣,個人目標就變成:湊夠 3-3=0 元須要的最少硬幣數量。即 d(3)=d(3-3)+1=d(0)+1=0+1=1. 這個方案說的是,我拿 1 個 3 元的硬幣。好了,這兩種方案哪一種更優呢?記得咱們但是要用最少的硬幣數量來湊夠 3元的。因此,選擇 d(3)=1 ,怎麼來的呢?具體是這樣獲得的: d(3)=min{d(3-1)+1, d(3-3)+1} 。設計
OK,碼了這麼多字講具體的東西,讓咱們來點抽象的。從以上的文字中,咱們要抽出動態規劃裏很是重要的兩個概念:狀態和狀態轉移方程。code
上文中d(i) 表示湊夠 i 元須要的最少硬幣數量,咱們將它定義爲該問題的 " 狀態 " ,這個狀態是怎麼找出來的呢?根據子問題定義狀態。你找到子問題,狀態也就浮出水面了。最終咱們要求解的問題,能夠用這個狀態來表示: d(11) ,即湊夠 11 元最少須要多少個硬幣。那狀態轉移方程是什麼呢?既然咱們用 d(i)表示狀態,那麼狀態轉移方程天然包含 d(i) ,上文中包含狀態 d(i) 的方程是: d(3)=min{d(3-1)+1, d(3-3)+1} 。沒錯,它就是狀態轉移方程,描述狀態之間是如何轉移的。固然,咱們要對它抽象一下,htm
d(i)=min{ d(i-v j )+1 },其中 i-v j >=0, v j 表示第j 個硬幣的面值 ;
有了狀態和狀態轉移方程,這個問題基本上也就解決了。固然了,Talk is cheap,show me the code!
下圖是當i 從 0 到 11 時的解:
從上圖能夠得出,要湊夠11 元至少須要 3 枚硬幣。
此外,經過追蹤咱們是如何從前一個狀態值獲得當前狀態值的,能夠找到每一次咱們用的是什麼面值的硬幣。好比,從上面的圖咱們能夠看出,最終結果d(11)=d(10)+1( 面值爲 1) ,而 d(10)=d(5)+1( 面值爲 5),最後 d(5)=d(0)+1 ( 面值爲 5) 。因此咱們湊夠 11 元最少須要的 3 枚硬幣是: 1 元、 5 元、 5 元。
經過硬幣問題咱們初識 DP的原理,其實能夠說貪心問題是 DP 問題的特例,如今咱們經過幾道題目加深對 DP 問題的理解
數塔問題 是動態規劃經典的題目,下面來初步講解下
將一個由N 行數字組成的三角形,如圖因此,設計一個算法,計算出三角形的由頂至底的一條路徑,使該路徑通過的數字總和最大。
學弟學妹們大家以前學過 DFS和 BFS ,第一眼看過去這題應該用 DFS 解決,沒錯, DFS 也能夠,可是咱們觀察下 n 行總共有 (1 + 2 + 3 + 4+...+n) = ( 1+n ) *n/2 個節點,在遞歸求解的過程當中不少節點被重複訪問了,這就致使時間大大增長,必然超時
好比用遞歸的話,18 這個節點被訪問了兩次
可是若是用DP的話這個節點能夠只訪問一次
好了,如今咱們用DP解決這道問題
將上圖轉化一下:
假設上圖用map[][] 數組保存。
令f[i][j] 表示從頂點 (1, 1) 到頂點 (i, j) 的最大值。
則能夠獲得狀態轉移方程:
f[i][j] = max(f[i+1][j], f[i+1][j+1]) + map[i][j]
此題既適合自頂而下的方法作,也適合自底而上的方法,
當用自頂而下的方法作時,最後要在在最後一列數中找出最大值,
而用自底而上的方法作時,f[1][1] 即爲最大值。
因此咱們將圖2 根據狀態轉移方程能夠獲得圖 3 :
最大值是30.
代碼以下:
上面討論了 兩個 很是簡單的例子。如今讓咱們來看看對於更復雜的問題,如何找到狀態之間的轉移方式(即找到狀態轉移方程 ) 。爲此咱們要引入一個新詞叫遞推關係來將狀態聯繫起來 ( 說的仍是狀態轉移方程)
OK,上例子,看看它是如何工做的。
一個序列有N 個數: A[1],A[2],…,A[N] ,求出最長非降子序列的長度。 ( 講 DP 基本都會講到的一個問題LIS : longest increasing subsequence)
正如上面咱們講的,面對這樣一個問題,咱們首先要定義一個「 狀態 」 來表明它的子問題,而且找到它的解。注意,大部分狀況下,某個狀態只與它前面出現的狀態有關,而獨立於後面的狀態。
讓咱們沿用「 入門 」 一節裏那道簡單題的思路來一步步找到 「 狀態 」 和 「 狀態轉移方程 」 。假如咱們考慮求A[1],A[2],…,A[i] 的最長非降子序列的長度,其中 i<N ,那麼上面的問題變成了原問題的一個子問題 ( 問題規模變小了,你可讓 i=1,2,3 等來分析 ) 而後咱們定義 d(i) ,表示前 i 個數中以 A[i] 結尾的最長非降子序列的長度。 OK ,對照 「 入門 」 中的簡單題,你應該能夠估計到這個 d(i) 就是咱們要找的狀態。若是咱們把 d(1) 到 d(N) 都計算出來,那麼最終咱們要找的答案就是這裏面最大的那個。狀態找到了,下一步找出狀態轉移方程。
爲了方便理解咱們是如何找到狀態轉移方程的,我先把下面的例子提到前面來說。若是咱們要求的這N 個數的序列是:
5, 3 , 4 , 8 , 6 , 7
根據上面找到的狀態,咱們能夠獲得:(下文的最長非降子序列都用LIS 表示)
· 前1 個數的 LIS 長度 d(1)=1( 序列: 5)
· 前2 個數的 LIS 長度 d(2)=1( 序列: 3 ; 3 前面沒有比 3 小的 )
· 前3 個數的 LIS 長度 d(3)=2( 序列: 3 , 4 ; 4 前面有個比它小的 3 ,因此 d(3)=d(2)+1)
· 前4 個數的 LIS 長度 d(4)=3( 序列: 3 , 4 , 8 ; 8 前面比它小的有 3 個數,因此d(4)=max{d(1),d(2),d(3)}+1=3)
OK,分析到這,我以爲狀態轉移方程已經很明顯了,若是咱們已經求出了 d(1) 到 d(i-1) ,那麼 d(i) 能夠用下面的狀態轉移方程獲得:
d(i) = max{1, d(j)+1},其中 j<i,A[j]<=A[i]
用大白話解釋就是,想要求d(i) ,就把 i 前面的各個子序列中,最後一個數不大於 A[i] 的序列長度加 1 ,而後取出最大的長度即爲 d(i) 。固然了,有可能 i 前面的各個子序列中最後一個數都大於 A[i] ,那麼d(i)=1 ,即它自身成爲一個長度爲 1 的子序列。
分析完了,上圖:( 第二列表示前 i 個數中 LIS 的長度,第三列表示, LIS 中到達當前這個數的上一個數的下標,根據這個能夠求出 LIS 序列 )
代碼:
該算法的時間複雜度是O(n 2 ),並非最優的解法。還有一種很巧妙的算法能夠將時間複雜度降到O(nlogn) ,網上已經有各類文章介紹它,這裏就再也不贅述。此題還能夠用 「 排序 +LCS」 來解,感興趣的話可自行 Google , Baidu 。
最後講一下最長上升公共子序列問題 :
什麼是最長公共子序列呢 ? 比如一個數列S,若是分別是兩個或多個已知數列的子序列,且是全部符合此條件序列中最長的,則S 稱爲已知序列的最長公共子序列。
舉個例子,如:有兩條隨機序列,如 1 3 4 5 5 , and 2 4 5 5 7 6 ,則它們的最長公共子序列即是: 4 5 5。
·
·
解最長公共子序列問題時最容易想到的算法是窮舉搜索法,即對 X 的每個子序列,檢查它是否也是 Y的子序列,從而肯定它是否爲 X 和 Y 的公共子序列,而且在檢查過程當中選出最長的公共子序列。 X 和 Y的全部子序列都檢查事後便可求出 X 和 Y 的最長公共子序列。 X 的一個子序列相應於下標序列{1, 2, …, m} 的一個子序列,所以, X 共有 2m個不一樣子序列(Y 亦如此,如爲 2^n ),從而窮舉搜索法須要指數時間( 2^m * 2^n )。
· 動態規劃算法
事實上,最長公共子序列問題也有最優子結構性質。
記:
Xi=﹤ x1 , ⋯ , xi ﹥即 X 序列的前 i 個字符 (1≤i≤m) (前綴)
Yj=﹤ y1 , ⋯ , yj ﹥即 Y 序列的前 j 個字符 (1≤j≤n) (前綴)
假定Z= ﹤ z1 , ⋯ , zk ﹥ ∈LCS(X , Y) 。
·
若 xm=yn (最後一個字符相同),則不難用反證法證實:該字符必是X 與 Y 的任一最長公共子序列 Z(設長度爲 k )的最後一個字符,即有 zk = xm = yn 且顯然有 Zk-1∈LCS(Xm-1 , Yn-1) 即 Z 的前綴 Zk-1是 Xm-1 與 Yn-1 的最長公共子序列 。此時,問題化歸成求Xm-1 與 Yn-1 的 LCS ( LCS(X , Y)的長度等於 LCS(Xm-1 , Yn-1 )的長度加 1 )。
·
·
若 xm≠yn ,則亦不難用反證法證實:要麼Z∈LCS(Xm-1, Y) ,要麼 Z∈LCS(X , Yn-1) 。因爲 zk≠xm 與zk≠yn 其中至少有一個必成立,若 zk≠xm 則有 Z∈LCS(Xm-1 , Y) ,相似的,若 zk≠yn 則有Z∈LCS(X , Yn-1) 。此時,問題化歸成求 Xm-1 與 Y 的 LCS 及 X 與 Yn-1 的 LCS 。 LCS(X , Y) 的長度爲: max{LCS(Xm-1 , Y) 的長度 , LCS(X , Yn-1) 的長度 } 。
·
因爲上述當xm≠yn的狀況中,求LCS(Xm-1 , Y) 的長度與 LCS(X , Yn-1) 的長度,這兩個問題不是相互獨立的:二者都須要求 LCS(Xm-1 , Yn-1) 的長度。另外兩個序列的 LCS 中包含了兩個序列的前綴的 LCS,故問題具備最優子結構性質考慮用動態規劃法。
也就是說,解決這個 LCS 問題,你要求三個方面的東西:一、LCS ( Xm-1 , Yn-1 ) +1 ;二、LCS (Xm-1 , Y ), LCS ( X , Yn-1 );三、max{LCS ( Xm-1 , Y ), LCS ( X , Yn-1 ) }。
行文至此,其實對這個 LCS 的動態規劃解法已敘述殆盡,不過,爲了成書的某種必要性,下面,我試着再多加詳細闡述這個問題。
3.一、最長公共子序列的結構
最長公共子序列的結構有以下表示:
設序列 X=<x1, x2, …, xm>和 Y=<y1, y2, …, yn>的一個最長公共子序列 Z=<z1, z2, …, zk>,則:
1. 若x m =y n ,則z k =x m =y n 且Z k-1 是X m-1 和Y n-1 的最長公共子序列;
2. 若x m ≠y n 且z k ≠x m , 則Z 是 X m-1 和Y 的最長公共子序列;
3. 若x m ≠y n 且z k ≠y n ,則 Z 是 X 和 Y n-1 的最長公共子序列。
其中 Xm-1=<x1, x2, …, xm-1>, Yn-1=<y1, y2, …, yn-1>, Zk-1=<z1, z2, …, zk-1>。
由最長公共子序列問題的最優子結構性質可知,要找出 X=<x1, x2, …, xm>和 Y=<y1, y2, …, yn>的最長公共子序列,可按如下方式遞歸地進行:當 xm=yn時,找出Xm-1和Yn-1的最長公共子序列,而後在其尾部加上xm(=yn)便可得 X 和 Y 的一個最長公共子序列。當 xm≠yn時,必須解兩個子問題,即找出Xm-1和Y 的一個最長公共子序列及 X 和 Yn-1的一個最長公共子序列。這兩個公共子序列中較長者即爲X 和 Y 的一個最長公共子序列。
由此遞歸結構容易看到最長公共子序列問題具備子問題重疊性質。例如,在計算 X 和 Y 的最長公共子序列時,可能要計算出 X 和 Yn-1及Xm-1和Y 的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算 Xm-1和Yn-1的最長公共子序列。
與矩陣連乘積最優計算次序問題相似,咱們來創建子問題的最優值的遞歸關係。用 c[i,j] 記錄序列 Xi和Yj的最長公共子序列的長度。其中Xi=<x1, x2, …, xi>, Yj=<y1, y2, …, yj>。當 i=0 或 j=0 時,空序列是 Xi和Yj的最長公共子序列,故c[i,j]=0 。其餘狀況下,由定理可創建遞歸關係以下:
代碼以下 :
講到這想必對 DP問題有一個大概的認識了吧?乘熱打鐵,咱們去 HDU 刷幾道簡單題練練手感!
HDU2191
HDU1159
HDU1432
HDU2084