前面用動態規劃
解決了正則表達式的問題,感受仍是不過癮,總以爲對於動態規劃
的理解尚未到位,因此趁熱打鐵,繼續研究幾個動態規劃
的經典問題,但願可以藉此加深對動態規劃
的理解。在此以前,還須要說兩個跟動態規劃有關的理論知識。java
最優化原理
指的最優策略具備這樣的性質:不論過去狀態和決策如何,對前面的決策所造成的狀態而言,餘下的諸決策必須構成最優策略。簡單來講就是一個最優策略的子策略也是必須是最優的,而全部子問題的局部最優解將致使整個問題的全局最優。若是一個問題能知足最優化原理
,就稱其具備最優子結構性質
。正則表達式
這是判斷問題可否使用動態規劃解決的先決條件,若是一個問題不能知足最優化原理,那麼這個問題就不適合用動態規劃來求解。算法
這樣說可能比較模糊,來舉個栗子吧:數組
如上圖,求從A點到E點的最短距離,那麼子問題就是求從A點到E點之間的中間點到E點的最短距離,好比這裏的B點。函數
那麼這個問題裏,怎麼證實最優化原理呢?優化
咱們假設從A點到E點的最短距離爲d,其最優策略的子策略假設通過B點,記該策略中B點到E點的距離爲d1
,A點到B點的距離爲d2
。咱們可使用反證法,假設存在B點到E點的最短距離d3
,而且d3 < d1
,那麼 d3 + d2 < d1 + d2 = d
,這與d是最短距離相矛盾,因此,d1
是B點到E點的最短距離。3d
爲了增長理解,這裏再舉一個反例:code
圖中有四個點,A、B、C、D,相鄰兩點有兩條連線,表明兩條通道,d1,d2,d3,d4,d5,d6表明的是道路的長度,求A到D的全部通道中,總長度除以4獲得的餘數最小的路徑爲最優路徑,求一條最優路徑。blog
這裏若是仍是按照上面的思路去求解,就會誤入歧途了。按照以前的思路,A的最優取值應該能夠由B的最優取值來肯定,而B的最優取值爲(3+5)mod 4 = 0。因此應該選d2
和d6
這兩條道路,而實際上,全局最優解是d4+d5+d6
或者d1+d5+d3
。因此這裏子問題的最優解並非原問題的最優解,即不知足最優化原理。因此就不適合使用動態規劃來求解了。遞歸
無後效性
指的是某狀態下決策的收益,只與狀態和決策相關,與到達該狀態的方式無關。某個階段的狀態一旦肯定,則此後過程的演變再也不受此前各類狀態及決策的影響。換句話說,將來與過去無關,當前狀態是此前歷史狀態的完整總結,此前歷史決策只能經過影響當前的狀態來影響將來的演變。再換句話說,過去作的選擇不會影響如今能作的最優選擇,如今能作的最優選擇只與當前的狀態有關,與通過如何複雜的決策到達該狀態的方式無關。
這也是用來驗證問題是否可使用動態規劃來解答的重要方法。
咱們再回頭看看上面的最短路徑問題,若是在原來的基礎上加上一個限制條件:同一個格子只能經過一次。那麼, 這個題就不符合無後效性了,由於前一個子問題的解會對後面子問題的選擇策略有影響,好比說,若是從A到B選擇了一條以下圖中綠色表示的路線,那麼從B點出發到達E點的路線就只有一條了。也就是說從A點到B點的路徑選擇會影響B點到E點的路徑選擇。
理論部分就此打住,接下來咱們實戰一下。
假設你是一名經驗豐富的探險家,揹着揹包來到野外進行平常探險。天氣晴朗而不燥熱,山間的風夾雜着花香,正當你欣賞這世外桃源般的美景時,忽然,你發現了一個洞穴,這個洞穴外表看起來其貌不揚,但憑藉着驚爲天人的直覺,這個洞穴不簡單。
因而,你開始往洞穴內探索,但願能發現一些有意思的東西。終於,皇天不負有心人,你在洞穴的盡頭,發現了一堆不世出的珠寶,憑藉你驚人的閱歷,一眼便看出了它們各自的價值,心想着下下下下下下下下半輩子都有着落了。
然而,天有不測風雲,正準備將它們收入囊中,卻不當心觸碰到一個防護機關,洞穴立刻就要崩塌了。在此危機時刻,你只有一個揹包,你必須儘快作出抉擇,從中選擇最值錢的珠寶塞到你的揹包,讓揹包中珠寶的總價值最大。
好了好了,囉裏囉嗦了大半天,我仍是來精簡一下問題吧。簡而言之,你只有一個容量有限的揹包,總容量爲c,有n個可待選擇的物品,每一個物品只有一件,它們都有各自的重量和價值,你須要從中選擇合適的組合來使得你揹包中的物品總價值最大。
那還不簡單,無論是什麼,先往揹包裏塞,塞滿趕忙走,狗命要緊,狗命要緊。。。
好了好了,開個玩笑,言歸正傳。
簡單起見,咱們來將上面的問題具體化,舉一個更具體的栗子:
假設有5個物品,它們的價值(v)和重量(w)以下圖:
揹包總容量爲10,如今要從中選擇物品裝入揹包中,要求物品的重量不能超過揹包的容量,而且最後放在揹包中物品的總價值最大。
emmm,等等,爲何叫作0/1揹包
呢?爲何不叫1/2揹包
,2/3揹包
???
仔細想一想,這裏每一個物品只有一個,對於每一個物品而言,只有兩種選擇,盤它或者不盤,盤它記爲1,不盤記爲0,咱們不能將物品進行分割,好比只拿半個是不容許的。這就是這個問題被稱爲0/1揹包
問題的緣由。
因此究竟選仍是不選,這是個問題。
讓咱們先來體驗一下將珠寶裝入揹包的感受,爲了方便起見,用xi
表明第i個珠寶的選擇(xi = 1
表明選擇該珠寶,0則表明不選),vi
表明第i個珠寶的價值,wi
表明第i個珠寶的重量。因而咱們就有了這樣的限制條件:
咱們的初始狀態是揹包容量爲10,揹包內物品總價值爲0,接下來,咱們就要開始作選擇了。對於1號珠寶,當前容量爲10,容納它的重量2綽綽有餘,所以有兩種選擇,選它或者不選。咱們選擇一個珠寶的時候,揹包的容量會減小,可是裏面的物品總價值會增長。就像下面這樣:
這樣就分出了兩種狀況,咱們繼續進行選擇,若是咱們選擇了珠寶1,那麼對於珠寶2,當前剩餘容量爲8,大於珠寶2的容量3,所以也有兩種選擇,選或者不選。
如今,咱們獲得了四個可能結果,咱們每作出一個選擇,就會將上面的每一種可能分裂成兩種可能,後續的選擇也是如此,最終,咱們會獲得以下的一張決策圖:
這裏被塗上色的方框表明咱們的最終待選結果,原本應該有16個待選結果,但有三個結果因爲容量不足以容納下最後一個珠寶,因此就沒有繼續進行裂變。
而後,咱們從這些結果中,找出價值最大的那個,也就是13
,這就是咱們的最優選擇,根據這個選擇,依次找到它的全部路徑,即可以知道該選哪幾個珠寶,最終結果是:珠寶4,珠寶2,珠寶1。
接下來,咱們就來分析一下,如何將它擴展到通常狀況。爲了實現這個目的,咱們須要將問題進行抽象並建模,而後將其劃分爲更小的子問題,找出遞推關係式,這是分治思想中很重要的一步。
那這裏的遞推關係式是怎樣的呢?對於第i個物品,有兩種可能:
對於這個問題的子問題,這裏有必要詳細說明一下。原問題是,將n件物品放入容量爲c的揹包,子問題則是,將前i件物品放入容量爲j的揹包,所獲得的最優價值爲KS(i,j),若是隻考慮第i件物品放仍是不放,那麼就能夠轉化爲一個只涉及到前i-1個物品的問題。若是不放第i個物品,那麼問題就轉化爲「前i-1件物品放入容量爲j的揹包中的最優價值組合」,對應的值爲KS(i-1,j)。若是放第i個物品,那麼問題就轉化成了「前i-1件物品放入容量爲j-wi的揹包中的最優價值組合」,此時對應的值爲KS(i-1,j-wi)+vi。
因此,就能夠很容易的寫出遞歸解法了:
public class Solution{ int[] vs = {0,2,4,3,7}; int[] ws = {0,2,3,5,5}; @Test public void testKnapsack1() { int result = ks(4,10); System.out.println(result); } private int ks(int i, int c){ int result = 0; if (i == 0 || c == 0){ // 初始條件 result = 0; } else if(ws[i] > c){ // 裝不下該珠寶 result = ks(i-1, c); } else { // 能夠裝下 int tmp1 = ks(i-1, c); int tmp2 = ks(i-1, c-ws[i]) + vs[i]; result = Math.max(tmp1, tmp2); } return result; } }
這裏爲了方便處理,將數組ws和vs都增長了一個補位數0,防止數組越界,輸出結果:
13
這樣,咱們就輕鬆加愉快的解決了這個問題。
既然開頭已經說了兩個驗證問題是否可使用動態規劃求解的方法,那麼爲什麼不試一試呢?
先來看看最優化原理
。一樣,咱們使用反證法:
假設(x1,x2,…,xn)是01揹包問題的最優解,則有(x2,x3,…,xn)是其子問題的最優解,假設(y2,y3,…,yn)是上述問題的子問題最優解,則有(v2y2+v3y3+…+vnyn)+v1x1 > (v2x2+v3x3+…+vnxn)+v1x1。說明(X1,Y2,Y3,…,Yn)纔是該01揹包問題的最優解,這與最開始的假設(X1,X2,…,Xn)是01揹包問題的最優解相矛盾,故01揹包問題知足最優性原理
。
至於無後效性
,其實比較好理解。對於任意一個階段,只要揹包剩餘容量和可選物品是同樣的,那麼咱們能作出的現階段的最優選擇一定是同樣的,是不受以前選擇了什麼物品所影響的。即知足無後效性
。
就像上一篇裏的解法同樣,自上而下的解法與分治法的區別就是增長了一個數組用來存儲計算的中間結果來減小重複計算。這裏,咱們只須要多定義一個二維數組。
表格中,每個格子都表明着一個子問題,咱們最終的問題是求最右下角的格子的值,也就是i=4,j=10
時的值。這裏,咱們的初始條件即是i=0或者j=0時對應的ks值爲0,這很好理解,若是可選物品爲0,或者剩餘容量爲0,那麼最大價值天然也是0。代碼以下:
public class Solution{ int[] vs = {0,2,4,3,7}; int[] ws = {0,2,3,5,5}; Integer[][] results = new Integer[5][11]; @Test public void testKnapsack2() { int result = ks2(4,10); System.out.println(result); } private int ks2(int i, int c){ int result = 0; // 若是該結果已經被計算,那麼直接返回 if (results[i][c] != null) return results[i][c]; if (i == 0 || c == 0){ // 初始條件 result = 0; } else if(ws[i] > c){ // 裝不下該珠寶 result = ks(i-1, c); } else { // 能夠裝下 int tmp1 = ks(i-1, c); int tmp2 = ks(i-1, c-ws[i]) + vs[i]; result = Math.max(tmp1, tmp2); results[i][c] = result; } return result; } }
能夠看到,其實只比分治多了三行代碼。
接下來,咱們用自下而上的方法來解一下這道題,思路很簡單,就是不斷的填表,回想一下上一篇中的斐波拉契數列的自下而上解法,這裏將使用一樣的方式來解決。仍是使用上面的表格,咱們開始一行行填表。
當i=1時,即只有珠寶1可供選擇,那麼若是容量足夠的話,最大價值天然就是珠寶1的價值了。
當i=2時,有兩個物品可供選擇,此時應用上面的遞推關係式進行判斷便可。這裏以i=2,j=3爲例進行分析:
剩下的格子使用相同的方法進行填充便可:
這樣,咱們就獲得了最後的結果:13。根據結果,咱們能夠反向找出各個物品的選擇,尋找的方法很簡單,就是從i=4,j=10
開始尋找,若是ks(i-1,j)=ks(i,j)
,說明第i個物品沒有被選中,從ks(i-1,j)
繼續尋找。不然,表示第i個物品已被選中,則從ks(i-1,j-wi)
開始尋找。
轉化成代碼:
public class Solution{ int[] vs = {0,2,4,3,7}; int[] ws = {0,2,3,5,5}; Integer[][] results = new Integer[5][11]; @Test public void testKnapsack3() { int result = ks3(4,10); System.out.println(result); } private int ks3(int i, int j){ // 初始化 for (int m = 0; m <= i; m++){ results[m][0] = 0; } for (int m = 0; m <= j; m++){ results[0][m] = 0; } // 開始填表 for (int m = 1; m <= i; m++){ for (int n = 1; n <= j; n++){ if (n < ws[m]){ // 裝不進去 results[m][n] = results[m-1][n]; } else { // 容量足夠 if (results[m-1][n] > results[m-1][n-ws[m]] + vs[m]){ // 不裝該珠寶,最優價值更大 results[m][n] = results[m-1][n]; } else { results[m][n] = results[m-1][n-ws[m]] + vs[m]; } } } } return results[i][j]; } }
嗯,完美解決。時間複雜度即填表耗時O(n * c)
,這裏用了一個二維數組來存儲子問題的解,因此空間複雜度爲O(n * c)
;
回過頭再看看上面的分析,會發現動態規劃裏最關鍵的問題實際上是尋找原問題的子問題,並寫出遞推表達式,只要完成了這一步,代碼部分都是水到渠成的事情了。
那麼問題來了,怎樣把問題拆分紅子問題呢?
emmm,這個問題有點超綱了,說實話,我也沒有掌握到訣竅,仍是得具體狀況具體分析,可是不少經典的問題都有其經典的套路,其它問題均可以歸結到這些問題上面來,能夠看作是它們的變種和延伸,把這些經典的問題吃透的話,天然能觸類旁通。好比採藥問題,本質上就是01揹包問題,而硬幣問題,本質上就是咱們以後要介紹的徹底揹包問題。
我的認爲,算法不在於刷多少個,而在於概括總結,就跟作數學題同樣,總有一些範式和套路,無論形式如何變化,其本質是同樣的,萬變不離其宗,說的就是這麼回事。
本篇到此就告一段落了,若是以爲有收穫,不要吝嗇你的贊哦,也歡迎關注個人公衆號留言交流。