做爲考察範圍最廣,考察次數最多的算法,固然要開一篇博客來複習啦。算法
子曰:溫故而知新,能夠爲師矣數組
我複習DP時有一些本身對DP的理解,也就分享出來吧。學習
——正片開始——優化
動態規劃算法,即Dynamic Programming(如下簡稱爲DP),是解決多階段決策過程最優化問題的高效數學方法。自從1999年IOI出了一道名爲"數字三角形"的題後,DP題就在OI競賽中廣爲流傳。而上面提到的"數字三角形",如今就是DP的一道入門題。spa
遞推和DP的關係:設計
不少人會混淆遞推和DP,遞推只是DP的一種實現方式。咱們提到的DP是一種高效的算法,實現方式主要是遞推和記憶化搜索,可是DP和遞推很像的一點就是,它們都是利用子問題來搞定原問題。code
我舉一個例子,斐波那契數列,相信你們都不陌生。blog
咱們知道Fib的遞推式:F(x) = F(x-1) + F(x-2),能夠經過第x-1項和第x-2項推出第x項。教程
若是咱們從DP的角度來看Fib,咱們能夠把求第x項當作原問題,而後咱們把求第x-1項和求第x-2項當作子問題,咱們就把"求第x項"這個原問題劃分紅了"求第x-1項"和"求第x-2項"兩個子問題,而後繼續劃分子問題並求解,最後利用全部的子問題獲得原問題的解。遞歸
不少DP的入門博客都會仔細詳解DP的三個基本性質,解題步驟和使用DP的特徵。
不明白這些的,我從@mengmengdastyle的blog中摘取了這一段給大家看:
(2) 動態規劃包含三個重要的概念:
- 最優子結構
- 邊界
- 狀態轉移方程
(3)解題的通常步驟是:
1. 找出最優解的性質,刻畫其結構特徵和最優子結構特徵;
2. 遞歸地定義最優值,刻畫原問題解與子問題解間的關係;
3. 以自底向上的方式計算出各個子問題、原問題的最優值,並避免子問題的重複計算;
4. 根據計算最優值時獲得的信息,構造最優解。
(4)使用動態規劃特徵:
1. 求一個問題的最優解
2. 大問題能夠分解爲子問題,子問題還有重疊的更小的子問題
3. 總體問題最優解取決於子問題的最優解(狀態轉移方程)
4. 從上往下分析問題,從下往上解決問題
5. 討論底層的邊界問題
以上。
本文注重思路和解題技巧,對於基本知識很少贅述。
咱們先來分析一道題,經過這道題讓你明白DP是用來幹什麼的。
給你一個高度爲x(1<=x<=2x10^5)的樓梯,每次能夠向上走1級或者2級,問你走到頂一共有多少種走法。
輸入樣例:
10
輸出樣例:
89
咱們來分析一下這道題,假如x=1,答案你能夠很輕鬆的獲得,等於1,若是x=2呢?答案是2,也很容易獲得。
可是咱們的x是能夠很大的,咱們不可能對於每一個x都手算出結果存下來。
因而咱們考慮分解問題,將原問題分解成若干子問題,而後對於每一個子問題也分解下去。
「大事化小,小事化了。」
那咱們怎麼分解這個問題呢?若是咱們要求走3級的走法數,首先咱們看看可否用走1級和走2級的方案數湊出走3級的方案數。因而,咱們發現,確實能夠。咱們走3級的走法數就是走1級的走法數+走2級的走法數。
至此這個問題得解了:走x級的走法數=走x-1級的走法數+走x-2級的走法數。
這題就是求一個斐波那契數列,咱們利用遞推獲得答案。
介紹更困難的題目前,我打算講一講DP自己。
在OI競賽中,咱們使用計算機計算答案。可是咱們用計算機只能記錄下一些狀態,咱們須要利用記錄下來的狀態去求解,因而咱們要嘗試把問題定義成一個狀態。不少人在講DP的時候,說須要「遞歸」的定義狀態。對於這種說法,咱們通常用兩個字來形容:扯淡。咱們確實須要定義狀態,可是,每一個問題均可以被定義成狀態,咱們要作的首先就是思考怎麼去定義它,通常來講就是思考咱們存什麼變量,算什麼變量,用這些變量怎麼得出解。
咱們通常把問題劃分紅不一樣階段,每一個階段有不一樣狀態。可是,咱們並不須要算出全部狀態存下來,咱們每一步只須要存最優的答案就好了,這就是DP用來求最優解的緣由。既然問題都是能夠劃分紅階段和狀態的,那麼,某一階段的最優解就必定能夠經過以前階段的最優解獲得。
可是,若是咱們僅經過前一個階段的答案算不出當前階段的答案呢?若是咱們須要前面全部階段的答案呢?
若是在問題的每一個階段,一個狀態均可以轉移到下一個階段的多個狀態,那咱們計算解的時間複雜度就是指數級別的,也就是說咱們並不能用DP來解決這個問題。這種,前面的決策會影響後面的狀況,就被稱爲有後效性。
還記得DP的三個要素之一嗎?無後效性。
記得搜索的入門題嗎?01迷宮。
若是咱們要找到從起點到終點的最短路徑,咱們能夠只保存當前階段的狀態嗎?顯然不行。想一想咱們當時作的時候保存的狀態?{x,y,step},以及一個vis數組。因爲題目要求咱們求的路徑最短,咱們必須知道以前走過的全部位置。
即便咱們當前在同一個位置,咱們以前走的路線不一樣,也是會影響到咱們後面選擇走的路徑的,由於咱們不會走已經標記vis過的格子了。
因此咱們必須保存每一個階段經歷過的全部狀態才能獲得下一個階段的解。
這就是有後效性的問題的一個例子。
若是咱們須要記錄以前全部的狀態,咱們的複雜度就是指數級的,可是DP呢?
咱們並不須要記錄以前的全部狀態,咱們當前的決策並不受以前狀態的組合的影響了,就能夠多項式時間內出答案了。
引用一段@X丶dalao的blog:
每一個階段只有一個狀態->遞推;
每一個階段的最優狀態都是由上一個階段的最優狀態獲得的->貪心;
每一個階段的最優狀態是由以前全部階段的狀態的組合獲得的->搜索;
每一個階段的最優狀態能夠從以前某個階段的某個或某些狀態直接獲得而無論以前這個狀態是如何獲得的->動態規劃。
每一個階段的最優狀態能夠從以前某個階段的某個或某些狀態直接獲得
這個性質叫作最優子結構;
而無論以前這個狀態是如何獲得的
這個性質叫作無後效性。
好了,如今咱們講題。
網上各類什麼,讓你完全學懂DP啊,特別的DP入門教程啊,其實都不如本身多寫點DP題來的實在...
下面我將從幾道例題開始,從易到難慢慢打開DP的大門。
1. 石子合併:
有n堆石子排成一列,每堆石子有一個重量w[i], 每次合併能夠合併相鄰的兩堆石子,一次合併的代價爲兩堆石子的重量和w[i]+w[i+1]。問安排怎樣的合併順序,可以使得總合並代價達到最小。
首先咱們來劃分階段,咱們有一坨長度爲n的石子堆,咱們每次合併後,石子堆的數量都會減小,那咱們就從這裏切入。
直觀地想,咱們可能會這樣劃分階段:
咱們要合併石子,確定就要找一個地方,把它兩邊的石子合併起來。
設f[x]表示合併了x次的最小總代價。馬上就能發現不對...咱們選定不一樣的地方來合併,每次的答案時不一樣的,也就是說f[x]的值不定,這時確定是得不到最優解的。有人可能會有疑問,f[x]不是定義成了最小定義的代價了嗎?
那你回去仔細看看上面說的關於狀態定義的內容。
因此咱們須要從新定義狀態,這裏給出一種劃分方法,咱們用f[i,j]表示合併區間左端點爲i,右端點爲j的這段區間合併成一堆石子的最優值。
爲何這麼定義呢?這就涉及到一類問題:區間DP。
對於區間DP,咱們利用區間長度做爲階段,用左右端點表示狀態。這種定義方法能夠解決大部分的區間DP問題了,可是遇到一些難題,咱們還須要加維度來解決。
咱們上面提到過,要合併兩堆石子,咱們就要循環一個分界點,咱們定義一個分界點k,枚舉這個分界點找最優解。這個過程咱們稱之爲——決策!
而後咱們利用決策轉移狀態(用子問題求解出原問題):
下面這個式子就是咱們常聽到的「狀態轉移方程」:
f[i,j] = f[i][k] + f[k+1][j] + cost(i,j),其中cost(i,j)表示合併兩堆石子的代價。
而後咱們思考一下狀態的可選範圍,i表示左端點,j表示右端點。
i: 1~n-len+1,j:i+len-1,這樣咱們就保證了既不超出邊界,又能保證咱們的階段是區間長度len。
階段就是len:2~n
這種作法時間複雜度是O(n^4),咱們發現無法簡化定義了,因而咱們O(n^3)預處理出cost(i,j),再O(n^3)DP得出答案。
僞代碼大概是這樣的:
for(i,1,n) for(j,i,n) for(k,i,j) cost(i,j)+=w[k] for(len,2,n) for(i,1,n-len+1) j=i+len-1 for(k,i,j) f[i][j]=min(f[i][j],f[i][k]+f[k+1][r]+cost(i,j))
若是仍是不太理解的能夠仔細去看看這道題的題解,博主這一篇博客只打算講思路,不仔細講例題。接下來咱們看這樣一道題:沒有上司的舞會
題目描述已經說了,它們的關係像一棵有根樹,那咱們就在樹上DP。這種依賴樹形結構的DP咱們也把它們劃分爲一類:
樹形DP。
這時候可能就有人想問了,既然也是一類DP,它的階段劃分是否是也和區間DP同樣,有套路呢?
沒錯。樹形DP依賴樹形結構,那麼咱們很容易想到樹的性質,父親和子節點的關係一一對應,咱們能夠經過子節點的信息計算父節點的信息。也就是,這類題已經幫咱們劃分好階段了,節點從深到淺的順序就是咱們的階段,咱們用一個從上到下的遍從來進行DP,對於每一個子節點x先往下遞歸在它的每一個子節點進行DP,再在回溯的時候從子節點向節點x轉移狀態。這樣咱們須要作的就是定義狀態了。定義狀態也很容易,咱們通常選擇每一個節點的編號x做爲狀態的第一維,再根據不一樣題目的需求加維進行DP。
回到這道題目上來,咱們根據上面的內容,先定義第一維爲節點編號,而後咱們會發現,一個節點的信息值只與它參不參加舞會有關係,因而咱們定義f[x][0/1]爲它參加/不參加時的值。
題目也明確說了,若是一我的的直接上司參加了舞會他就不會參加,那咱們就能夠輕鬆的獲得狀態轉移方程:
f[x][0]+=max(f[y][0],f[y][1]),f[x][1]+=f[y][0],y∈son(x)
而後遞歸求解就行了。
寫到這裏我發現我實在是講不完全部的DP類型了,因而咱們後面會跳過幾種DP分爲下一篇來談。
放到下一篇講的DP(狀壓DP,計數DP,數位DP,機率與指望DP,全部優化方法)
那麼咱們就回過來說DP中一類很特殊的問題:揹包問題
什麼是揹包問題?咱們先從基礎的0/1揹包開始,逐步分析揹包問題的模型。
給你n個物品,其中,第i個物品的體積爲wi,價值爲vi。再給你一個容積爲m的揹包,如今讓你在不超過容積的範圍內選出一些物品裝入揹包讓價值儘量大。
首先咱們可能會想到用貪心來解決這道題目,可是貪心很顯然是錯誤的。
咱們貪心的策略很顯然是每次選擇「性價比」最高的物品,也就是wi/vi最大的物品。
可是,對於0/1揹包問題,貪心選擇之因此不能獲得最優解是由於:
它沒法保證最終能將揹包裝滿,部分閒置的揹包空間使每公斤揹包空間的價值下降了。
明白這一點後,咱們考慮用DP求解。
如何劃分階段呢?很顯然,咱們依次考慮是否選擇每件物品,而後咱們還須要知道如今揹包用了多少容積。
那咱們就設f[i][j]爲前i件物品中裝了j體積的物品的最大價值。
這裏咱們用另一種思路來想轉移方程式,咱們不分解這個問題,咱們直接考慮狀態怎麼推動。
咱們前i件物品中裝j體積的最大價值很顯然是由前i-1件物品裝了某體積的物品,再考慮選不選擇當前這件物品轉移過來的。
那咱們很容易就能夠獲得以下的轉移方程:
f[i][j]=max(
f[i-1][j],//選這件物品
if(j>=wi)
f[i-1][j-wi]+v[i] //不選這件物品
else
f[i-1][j] //選不了這件物品
)
階段:前i件物品(i∈[1,n])
狀態:當前的容積(j∈[m,0])
這樣咱們的空間複雜度是O(n^2)的,考慮優化空間。
咱們發現第一位是能夠省略的(咱們按順序依次考慮每一個物品便可)。
因而...就變成了這樣:
for(int i=1;i<=n;i++) for(int j=m;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+v[i]);
咱們每一個物品只能放一次,而咱們的f[j]要經過f[j-w[i]]計算獲得。
若是,咱們使用正序循環,從w[i]到m,那麼咱們可能出現這種狀況:
f[j]被f[j-wi]+vi更新過,當咱們j增長到j+w[i]時,f[j+w[i]]又有可能被f[j]+vi更新,而同時它們都處於階段i,也就是說,咱們在一個階段內的兩個狀態間發生了轉移,至關於第i個物品被使用了屢次(若是後面又更新了),不符合0/1揹包的要求。
因此咱們要倒序循環,這樣咱們的j會一直縮小,不會出現「同階段間轉移」的狀況,因此,問題至此得解。
接下來咱們要講徹底揹包,思考這樣一個問題:
給你n種物品,每種物品有無數個,其中,第i種物品的體積爲wi,價值爲vi。再給你一個容積爲m的揹包,如今讓你在不超過容積的範圍內選出一些物品裝入揹包讓價值儘量大。
注意它和0/1揹包問題的區別,0/1揹包每種物品只有一個,而徹底揹包有無數個。
細心的讀者可能發現了,咱們上面說,當咱們正序循環時,至關於一個物品被使用了屢次,符合徹底揹包的要求...那咱們只要正序循環,是否是就?
仍是太想固然了啊,固然沒錯。
而後咱們講講多重揹包吧,仍是一個相似的問題:
給你n種物品,每種物品有ci個,其中,第i種物品的體積爲wi,價值爲vi。再給你一個容積爲m的揹包,如今讓你在不超過容積的範圍內選出一些物品裝入揹包讓價值儘量大。
每種物品從無數個變成了ci個,也就是有了限制,怎麼作呢?
水題啊!咱們把每種物品當作ci個不一樣的物品不就行了?而後跑一遍0/1揹包,問題不久得解了咩?
天真啊,這樣的時間複雜度但是 $O(m*\sum\limits_{i=1}^nc_i)$ 的啊(第一次用Latex有點不習慣)
那咋整啊?咱們又延伸出了:單調隊列優化DP。
DP的種類真是數不勝數...不過優化DP是下一篇的內容,這裏再也不敘述。
不想用單調隊列優化DP來解決多重揹包的話,咱們能夠二進制拆分多重揹包。
咱們大概是這樣一個拆分思路,把每一種物品拆成log個不一樣物品。
大概是這樣拆:
int cnt=0; for(int i=1;i<=n;i++){ int a,b,c; cin>>a>>b>>c; for(int j=1;j<=c;j<<=1){ v[++cnt]=j*a,w[cnt]=j*b; c-=j; } if(c)//剩下拆不掉的部分,直接當新物品 v[++cnt]=c*a,w[cnt]=c*b; }
思路仍是很簡單的,可是很巧妙。拆完就是一個0/1揹包了,很水。
我佛了...還有個分組揹包沒講...這篇博客都寫了2天了QuQ,DP真難講。
限於篇幅和時間,樹上的揹包問題我留到下一篇的開頭...
給出分組揹包的模型:
給你n組物品,每組物品有ci個,其中,第i組第j個物品的體積爲wi,j,價值爲vi,j。再給你一個容積爲m的揹包,如今讓你在不超過容積的範圍內每組至多選一個物品裝入揹包讓價值儘量大。
分組揹包是一類樹形DP的很重要的組成Part,因此熟練掌握它仍是很重要滴。
這個問題咱們怎麼作呢?
考慮用線性DP解決(...霧),咱們要知足「每組至多選擇一個物品」的要求,就能夠利用「階段」線性增加的特性,把物品組數做爲階段,只要選了一個第i組的物品,就轉移狀態到下一個階段就行了^-^。而後仿照0/1揹包的作法,設f[i][j]表示從前i組中選出整體積爲j的物品放入了揹包,物品的最大價值和。
f[i,j]=max{
f[i-1,j],//不選第i組的物品喵
max{f[i-1,j-wik]+vik},(1<=k<=ci)//選第i組的某個物品k喵->是作決策噠
}
上面那東西不是我敲的...可是她不讓我刪掉
咱們仍是能夠省略掉第一維,別問,問就是這是揹包問題。爲何呢?由於咱們能夠用j的倒序循環來控制階段i的狀態只能由階段i-1轉移獲得。
至此問題得解,給出代碼:
for(int i=1;i<=n;i++) for(int j=m;j>=0;j--) for(int k=1;k<=c[i];k++) if(j>=w[i][k]) f[j]=max(f[j],f[j-w[i][k]+v[i][k]);
總結一下,這篇博客咱們接觸並初步學習了動態規劃算法,並對DP的本質有了必定的瞭解,明白了設計DP算法求解問題的通常思路。沒錯,設計DP算法,DP算法迷人的地方就在於,對於每道DP題,都須要本身去設計一個合理且高效的DP算法去解決問題,這也是DP難的地方。除此以外,咱們還學習了幾種常見的DP模型,加深了對「階段,狀態,決策」的理解。DP題要難能夠難上天,要簡單能夠一眼秒,可是本質上看它們都是考察同一個東西:腦子。DP題其實不難個鬼,只要你理解了DP的基本實現方法,稍加思考,把問題轉化一下,就很容易想到如何用DP去求解答案。
對於這種依靠思惟的題目,其實不用寫不少題。雖然刷題是必不可缺的,可是對於寫過的每道DP題,都確保本身理解了思路,明白了爲何這樣設計DP,就能夠總結出一套本身應對DP題的方法和技巧,每一個人寫DP題的方法都是不盡相同的。但願經過這篇博客,能讓你喜歡上動態規劃算法。
更深層次難度更高的DP,我會在下一篇博客裏討論。不過就連這篇博客我都寫了整整兩天,下一篇可能我要寫挺久了。除非我夠肝。
大家的點贊就是我最大的動力(其實我就是想本身整理整理...),感謝大家的支持。