在上一篇中,咱們對01揹包問題進行了比較深刻的研究,這一篇裏,咱們來聊聊另外一個揹包問題:徹底揹包。java
有N種物品和一個容量爲T的揹包,每種物品都就能夠選擇任意多個,第i種物品的價值爲P[i],體積爲V[i],求解:選哪些物品放入揹包,可卡因使得這些物品的價值最大,而且體積總和不超過揹包容量。算法
跟01揹包同樣,徹底揹包也是一個很經典的動態規劃問題,不一樣的地方在於01揹包問題中,每件物品最多選擇一件,而在徹底揹包問題中,只要揹包裝得下,每件物品能夠選擇任意多件。從每件物品的角度來講,與之相關的策略已經再也不是選或者不選了,而是有取0件、取1件、取2件...直到取⌊T/Vi⌋(向下取整)件。數組
看到能夠選擇任意多件,你也許會想,那還不容易,選性價比最高的就行了。優化
因而開啓貪婪模式,把每種物品的價格除以體積來算出它們各自的性價比,而後只選擇性價比最高的物品放入揹包中。3d
嗯,聽起來好像沒什麼毛病,但仍舊有一個問題,那就是同一種物品雖然能夠選擇任意多件,但仍舊只能以件爲單位,也就是說單個物品是沒法拆分的,不能選擇半件,只能多選一件或者少選一件。這樣就形成了一個問題,每每沒法用性價比最高的物品來裝滿整個揹包,好比揹包空間爲10,性價比最高的物品佔用空間爲7,那麼剩下的空間該如何填充呢?code
你固然會想到用性價比第二高的物品填充,若是仍舊沒法填滿,那就依次用第3、第四性價比物品來填充。blog
聽起來彷佛可行,但我只須要舉一個反例便能證實這個策略行不通。遞歸
想要舉反例很簡單,好比只有兩個物品:物品A:價值5,體積5,物品B:價值8:體積7,揹包容量爲10,物品B的性價比顯然要比物品A高,那麼用貪心算法必然會選擇放入一個物品B,此時,剩餘的空間已沒法裝下A或者B,因此獲得的最高價值爲8,而實際上,選擇放入兩個物品A便可獲得更高的價值10。因此這裏貪心算法並不適用。class
像上一篇中的那樣,咱們只須要找到遞推關係式,就很容易使用遞歸解法來求解了。原理
用ks(i,t)表示前i種物品放入一個容量爲t的揹包得到的最大價值,那麼對於第i種物品,咱們有k種選擇,0 <= k * V[i] <= t,便可以選擇0、一、2...k個第i種物品,因此遞推表達式爲:
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)
同時,ks(0,t)=0;ks(i,0)=0;
使用上面的栗子,咱們能夠先用遞歸來求解:
public static class Knapsack { private static int[] P={0,5,8}; private static int[] V={0,5,7}; private static int T = 10; @Test public void soleve1() { int result = ks(P.length - 1,10); System.out.println("最大價值爲:" + result); } private int ks(int i, int t){ int result = 0; if (i == 0 || t == 0){ // 初始條件 result = 0; } else if(V[i] > t){ // 裝不下該珠寶 result = ks(i-1, t); } else { // 能夠裝下 // 取k個物品i,取其中使得總價值最大的k for (int k = 0; k * V[i] <= t; k++){ int tmp2 = ks(i-1, t - V[i] * k) + P[i] * k; if (tmp2 > result){ result = tmp2; } } } return result; } }
一樣,這裏的數組P和V分別添加了一個元素0,是爲了減小越界判斷而作的簡單處理,運行以下:
最大價值爲:11
若是你對比一下01揹包問題中的遞歸解法,就會發現惟一的區別即是這裏多了一層循環,由於01揹包中,對於第i個物品只有選和不選兩種狀況,只須要從這兩種選擇中選出最優的便可,而徹底揹包問題則須要在k種選擇中選出最優解,這即是最內層循環在作的事情。
for (int k = 0; k * V[i] <= t; k++){ // 選取k個第i件商品的最優價值爲tmp2 int tmp2 = ks(i-1, t - V[i] * k) + P[i] * k; if (tmp2 > result){ // 從中拿出最大的值即爲最優解 result = tmp2; } }
那這個問題能夠不能夠像01揹包問題同樣使用動態規劃來求解呢?來證實一下便可。
首先,先用反證法證實最優化原理:
假設徹底揹包的解爲F(n1,n2,...,nN)(n1,n2 分別表明第一、第2件物品的選取數量),徹底揹包的子問題爲,將前i種物品放入容量爲t的揹包並取得最大價值,其對應的解爲:F(n1,n2,...,ni),假設該解不是子問題的最優解,即存在另外一組解F(m1,m2,...,mi),使得F(m1,m2,...,mi) > F(n1,n2,...,ni),那麼F(m1,m2,...,mi,...,nN) 必然大於 F(n1,n2,...,nN),所以 F(n1,n2,...,nN) 不是原問題的最優解,與原假設不符,因此F(n1,n2,...,ni)必然是子問題的最優解。
再來看看無後效性:
對於子問題的任意解,都不會影響後續子問題的解,也就是說,前i種物品如何選擇,只要最終的剩餘揹包空間不變,就不會影響後面物品的選擇。即知足無後效性。
所以,徹底揹包問題也可使用動態規劃來解決。
既然知道了可使用動態規劃求解,接下來就是要找到這個問題的狀態轉移方程。
其實前面的遞推法中,已經找到了遞推關係式,它便已是咱們須要的狀態轉移方程。
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)
public static class Knapsack { private static int[] P={0,5,8}; private static int[] V={0,5,7}; private static int T = 10; private Integer[][] results = new Integer[P.length + 1][T + 1]; @Test public void solve2() { int result = ks2(P.length - 1,10); System.out.println("最大價值爲:" + result); } private int ks2(int i, int t){ // 若是該結果已經被計算,那麼直接返回 if (results[i][t] != null) return results[i][t]; int result = 0; if (i == 0 || t == 0){ // 初始條件 result = 0; } else if(V[i] > t){ // 裝不下該珠寶 result = ks2(i-1, t); } else { // 能夠裝下 // 取k個物品,取其中使得價值最大的 for (int k = 0; k * V[i] <= t; k++){ int tmp2 = ks2(i-1, t - V[i] * k) + P[i] * k; if (tmp2 > result){ result = tmp2; } } } results[i][t] = result; return result; } }
找出遞歸解法後,動態規劃的解法其實就很簡單了,只是多使用了一個二維數組來存儲中間的解。
最後,還可使用填表法來解決,此時須要將數組P和V額外添加的元素0去掉。
爲了方便理解,仍是再畫一個圖吧:
對於第i種物品,咱們能夠選擇的目標實際上是從上一層中的某幾個位置挑選出價值最高的一個。
這裏當t=10時,由於最多隻能放得下1個i2物品,因此只須要將兩個數值進行比較,若是t=14,那麼就須要將取0個、1個和兩個i2物品的狀況進行比較,而後選出最大值。
public static class Knapsack { private static int[] P={5,8}; private static int[] V={5,7}; private static int T = 10; private int[][] dp = new int[P.length + 1][T + 1]; @Test public void solve3() { for (int i = 0; i < P.length; i++){ for (int j = 0; j <= T; j++){ for (int k = 0; k * V[i] <= j; k++){ dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]); } } } System.out.println("最大價值爲:" + dp[P.length][T]); } }
跟01揹包問題同樣,徹底揹包的空間複雜度也能夠進行優化,具體思路這裏就不重複介紹了,能夠翻看前面的01揹包問題優化篇。
優化後的狀態轉移方程爲:
ks(t) = max{ks(t), ks(t - Vi) + Pi}
public static class Knapsack { private static int[] P={0,5,8}; private static int[] V={0,5,7}; private static int T = 10; private int[] newResults = new int[T + 1]; @Test public void resolve4() { int result = ksp(P.length,T); System.out.println(result); } private int ksp(int i, int t){ // 開始填表 for (int m = 0; m < i; m++){ for (int n = V[m]; n <= t; n++){ newResults[n] = Math.max(newResults[n] , newResults[n - V[m]] + P[m]); } // 能夠在這裏輸出中間結果 System.out.println(JSON.toJSONString(newResults)); } return newResults[newResults.length - 1]; } }
輸出以下:
[0,0,0,0,0,0,0,0,0,0,0] [0,0,0,0,0,5,5,5,5,5,10] [0,0,0,0,0,5,5,8,8,8,10] 10
其實徹底揹包問題也能夠轉化成01揹包問題來求解,由於第i件物品最多選 ⌊T/Vi⌋(向下取整) 件,因而能夠把第i種物品轉化爲⌊T/Vi⌋件體積和價值相同的物品,而後再來求解這個01揹包問題。具體方法這裏就很少說了,留給你們自行解決。若是遇到問題,能夠翻開前面關於01揹包問題的兩篇文章。
徹底揹包問題跟01揹包有不少類似之處,比較一下他們的狀態轉移方程以及各類解法,就會發現他們實際上是異父異母的親兄弟。
這兩個揹包問題的關鍵都在於狀態轉移方程的尋找,若是對於相似的問題沒有思路,能夠先嚐試找出遞歸解法,而後自上而下的記憶法便水到渠成了。
固然,最重要的仍是解題思路,理解記憶法和填表法的精髓,有助於以後觸類旁通,去解決相似的延伸問題。
關於徹底揹包問題的解析到此就結束了,祝你們五一愉快!
若是有疑問或者有什麼想法,也歡迎關注個人公衆號進行留言交流: