咱們遇到的問題中,有很大一部分能夠用動態規劃(簡稱DP)來解。解決這類問題能夠很大地提高你的能力與技巧,我會試着幫助你理解如何使用DP來解題。這篇文章是基於實例展開來說的,由於乾巴巴的理論實在很差理解。php
注意:若是你對於其中某一節已經瞭解而且不想閱讀它,不要緊,直接跳過它便可。ios
什麼是動態規劃,咱們要如何描述它?算法
動態規劃算法一般基於一個遞推公式及一個或多個初始狀態。當前子問題的解將由上一次子問題的解推出。使用動態規劃來解題只須要多項式時間複雜度,所以它比回溯法、暴力法等要快許多。數組
如今讓咱們經過一個例子來了解一下DP的基本原理。ui
首先,咱們要找到某個狀態的最優解,而後在它的幫助下,找到下一個狀態的最優解。spa
「狀態」表明什麼及如何找到它?.net
「狀態"用來描述該問題的子問題的解。原文中有兩段做者闡述得不太清楚,跳過直接上例子。翻譯
若是咱們有面值爲1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元? (表面上這道題能夠用貪心算法,但貪心算法沒法保證能夠求出解,好比1元換成2元的時候)code
首先咱們思考一個問題,如何用最少的硬幣湊夠i元(i<11)?爲何要這麼問呢?兩個緣由:1.當咱們遇到一個大問題時,老是習慣把問題的規模變小,這樣便於分析討論。 2.這個規模變小後的問題和原來的問題是同質的,除了規模變小,其它的都是同樣的,本質上它仍是同一個問題(規模變小後的問題實際上是原問題的子問題)。blog
好了,讓咱們從最小的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,碼了這麼多字講具體的東西,讓咱們來點抽象的。從以上的文字中,咱們要抽出動態規劃裏很是重要的兩個概念:狀態和狀態轉移方程。
上文中d(i)表示湊夠i元須要的最少硬幣數量,咱們將它定義爲該問題的"狀態",這個狀態是怎麼找出來的呢?我在另外一篇文章 動態規劃之揹包問題(一)中寫過:根據子問題定義狀態。你找到子問題,狀態也就浮出水面了。最終咱們要求解的問題,能夠用這個狀態來表示:d(11),即湊夠11元最少須要多少個硬幣。那狀態轉移方程是什麼呢?既然咱們用d(i)表示狀態,那麼狀態轉移方程天然包含d(i),上文中包含狀態d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。沒錯,它就是狀態轉移方程,描述狀態之間是如何轉移的。固然,咱們要對它抽象一下,
d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第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元。
注意:原文中這裏原本還有一段的,但我反反覆覆讀了幾遍,大概的意思我已經在上文從i=0到i=3的分析中有所體現了。做者原本想講的通俗一些,結果沒寫好,反而更很差懂,因此這段不翻譯了。
上面討論了一個很是簡單的例子。如今讓咱們來看看對於更復雜的問題,如何找到狀態之間的轉移方式(即找到狀態轉移方程)。爲此咱們要引入一個新詞叫遞推關係來將狀態聯繫起來(說的仍是狀態轉移方程)
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表示)
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序列)
Talk is cheap, show me the code:
#include <iostream> using namespace std; int lis(int A[], int n){ int *d = new int[n]; int len = 1; for(int i=0; i<n; ++i){ d[i] = 1; for(int j=0; j<i; ++j) if(A[j]<=A[i] && d[j]+1>d[i]) d[i] = d[j] + 1; if(d[i]>len) len = d[i]; } delete[] d; return len; } int main(){ int A[] = { 5, 3, 4, 8, 6, 7 }; cout<<lis(A, 6)<<endl; return 0; }
該算法的時間複雜度是O(n2 ),並非最優的解法。還有一種很巧妙的算法能夠將時間複雜度降到O(nlogn),網上已經有各類文章介紹它,這裏就再也不贅述。傳送門: LIS的O(nlogn)解法。此題還能夠用「排序+LCS」來解,感興趣的話可自行Google。
練習題
無向圖G有N個結點(1<N<=1000)及一些邊,每一條邊上帶有正的權重值。找到結點1到結點N的最短路徑,或者輸出不存在這樣的路徑。
提示:在每一步中,對於那些沒有計算過的結點,及那些已經計算出從結點1到它的最短路徑的結點,若是它們間有邊,則計算從結點1到未計算結點的最短路徑。
嘗試解決如下來自topcoder競賽的問題:
接下來,讓咱們來看看如何解決二維的DP問題。
平面上有N*M個格子,每一個格子中放着必定數量的蘋果。你從左上角的格子開始,每一步只能向下走或是向右走,每次走到一個格子上就把格子裏的蘋果收集起來,這樣下去,你最多能收集到多少個蘋果。
解這個問題與解其它的DP問題幾乎沒有什麼兩樣。第一步找到問題的「狀態」,第二步找到「狀態轉移方程」,而後基本上問題就解決了。
首先,咱們要找到這個問題中的「狀態」是什麼?咱們必須注意到的一點是,到達一個格子的方式最多隻有兩種:從左邊來的(除了第一列)和從上邊來的(除了第一行)。所以爲了求出到達當前格子後最多能收集到多少個蘋果,咱們就要先去考察那些能到達當前這個格子的格子,到達它們最多能收集到多少個蘋果。 (是否是有點繞,但這句話的本質實際上是DP的關鍵:欲求問題的解,先要去求子問題的解)
通過上面的分析,很容易能夠得出問題的狀態和狀態轉移方程。狀態S[i][j]表示咱們走到(i, j)這個格子時,最多能收集到多少個蘋果。那麼,狀態轉移方程以下:
S[i][j]=A[i][j] + max(S[i-1][j], if i>0 ; S[i][j-1], if j>0)
其中i表明行,j表明列,下標均從0開始;A[i][j]表明格子(i, j)處的蘋果數量。
S[i][j]有兩種計算方式:1.對於每一行,從左向右計算,而後從上到下逐行處理;2. 對於每一列,從上到下計算,而後從左向右逐列處理。這樣作的目的是爲了在計算S[i][j]時,S[i-1][j]和S[i][j-1]都已經計算出來了。
僞代碼以下:
如下兩道題來自topcoder,練習用的。
這一節要討論的是帶有額外條件的DP問題。
如下的這個問題是個很好的例子。
無向圖G有N個結點,它的邊上帶有正的權重值。
你從結點1開始走,而且一開始的時候你身上帶有M元錢。若是你通過結點i,那麼你就要花掉S[i]元(能夠把這想象爲收過路費)。若是你沒有足夠的錢,就不能從那個結點通過。在這樣的限制條件下,找到從結點1到結點N的最短路徑。或者輸出該路徑不存在。若是存在多條最短路徑,那麼輸出花錢數量最少的那條。限制:1<N<=100 ; 0<=M<=100 ; 對於每一個i,0<=S[i]<=100;正如咱們所看到的,若是沒有額外的限制條件(在結點處要收費,費用不足還不給過),那麼,這個問題就和經典的迪傑斯特拉問題同樣了(找到兩結點間的最短路徑)。在經典的迪傑斯特拉問題中,咱們使用一個一維數組來保存從開始結點到每一個結點的最短路徑的長度,即M[i]表示從開始結點到結點i的最短路徑的長度。然而在這個問題中,咱們還要保存咱們身上剩餘多少錢這個信息。所以,很天然的,咱們將一維數組擴展爲二維數組。M[i][j]表示從開始結點到結點i的最短路徑長度,且剩餘j元。經過這種方式,咱們將這個問題規約到原始的路徑尋找問題。在每一步中,對於已經找到的最短路徑,咱們找到它所能到達的下一個未標記狀態(i,j),將它標記爲已訪問(以後再也不訪問這個結點),而且在能到達這個結點的各個最短路徑中,找到加上當前邊權重值後最小值對應的路徑,即爲該結點的最短路徑。 (寫起來真是繞,建議畫個圖就會明瞭不少)。不斷重複上面的步驟,直到全部的結點都訪問到爲止(這裏的訪問並非要求咱們要通過它,好比有個結點收費很高,你沒有足夠的錢去通過它,但你已經訪問過它) 最後Min[N-1][j]中的最小值便是問題的答案(若是有多個最小值,即有多條最短路徑,那麼選擇j最大的那條路徑,即,使你剩餘錢數最多的最短路徑)。
僞代碼:
下面有幾道topcoder上的題以供練習:
如下問題須要仔細的揣摩才能將其規約爲可用DP解的問題。
問題:StarAdventure – SRM 208 Div 1:
給定一個M行N列的矩陣(M*N個格子),每一個格子中放着必定數量的蘋果。你從左上角的格子開始,只能向下或向右走,目的地是右下角的格子。你每走過一個格子,就把格子上的蘋果都收集起來。而後你從右下角走回左上角的格子,每次只能向左或是向上走,一樣的,走過一個格子就把裏面的蘋果都收集起來。最後,你再一次從左上角走到右下角,每過一個格子一樣要收集起裏面的蘋果 (若是格子裏的蘋果數爲0,就不用收集)。求你最多能收集到多少蘋果。
注意:當你通過一個格子時,你要一次性把格子裏的蘋果都拿走。
限制條件:1 < N, M <= 50;每一個格子裏的蘋果數量是0到1000(包含0和1000)。
若是咱們只須要從左上角的格子走到右下角的格子一次,而且收集最大數量的蘋果,那麼問題就退化爲「中級」一節裏的那個問題。將這裏的問題規約爲「中級」裏的簡單題,這樣一來會比較好解。讓咱們來分析一下這個問題,要如何規約或是修改才能用上DP。首先,對於第二次從右下角走到左上角得出的這條路徑,咱們能夠將它視爲從左上角走到右下角得出的路徑,沒有任何的差異。 (即從B走到A的最優路徑和從A走到B的最優路徑是同樣的)經過這種方式,咱們獲得了三條從頂走到底的路徑。對於這一點的理解能夠稍微減少問題的難度。因而,咱們能夠將這3條路徑記爲左,中,右路徑。對於兩條相交路徑(以下圖):
在不影響結果的狀況下,咱們能夠將它們視爲兩條不相交的路徑:
這樣一來,咱們將獲得左,中,右3條路徑。此外,若是咱們要獲得最優解,路徑之間不能相交(除了左上角和右下角必然會相交的格子)。所以對於每一行y( 除了第一行和最後一行),三條路徑對應的x座標要知足:x1[y] < x2[y] < x3[y]。通過這一步的分析,問題的DP解法就進一步地清晰了。讓咱們考慮行y,對於每個x1[y-1],x2[y-1]和x3[y-1],咱們已經找到了能收集到最多蘋果數量的路徑。根據它們,咱們能求出行y的最優解。如今咱們要作的就是找到從一行移動到下一行的方式。令Max[i][j][k]表示到第y-1行爲止收集到蘋果的最大數量,其中3條路徑分別止於第i,j,k列。對於下一行y,對每一個Max[i][j][k] 都加上格子(y,i),(y,j)和(y,k)內的蘋果數量。所以,每一步咱們都向下移動。咱們作了這一步移動以後,還要考慮到,一條路徑是有可能向右移動的。 (對於每個格子,咱們有多是從它上面向下移動到它,也多是從它左邊向右移動到它)。爲了保證3條路徑互不相交,咱們首先要考慮左邊的路徑向右移動的狀況,而後是中間,最後是右邊的路徑。爲了更好的理解,讓咱們來考慮左邊的路徑向右移動的狀況,對於每個可能的j,k對(j<k),對每一個i(i<j),考慮從位置(i-1,j,k)移動到位置(i,j,k)。處理完左邊的路徑,再處理中間的路徑,最後處理右邊的路徑。方法都差很少。
用於練習的topcoder題目:
當閱讀一個題目而且開始嘗試解決它時,首先看一下它的限制。若是要求在多項式時間內解決,那麼該問題就極可能要用DP來解。遇到這種狀況,最重要的就是找到問題的「狀態」和「狀態轉移方程」。(狀態不是隨便定義的,通常定義完狀態,你要找到當前狀態是如何從前面的狀態獲得的,即找到狀態轉移方程)若是看起來是個DP問題,但你卻沒法定義出狀態,那麼試着將問題規約到一個已知的DP問題(正如「高級」一節中的例子同樣)。
看完這教程離DP專家還差得遠,好好coding纔是王道。
來源:http://blog.csdn.net/cangchen/article/details/45045315