前面已經介紹完了01揹包和徹底揹包,今天介紹最後一種揹包問題——多重揹包。java
這個揹包,聽起來就很麻煩的樣子。別慌,只要你理解了前面的兩種揹包問題,拿下多重揹包簡直小菜一碟。算法
若是沒有看過前兩篇01揹包和徹底揹包的文章,強烈建議先閱讀一下,由於本文跟前兩篇文章關聯性很強。數組
有N種物品和一個容量爲T的揹包,第i種物品最多有M[i]件可用,價值爲P[i],體積爲V[i],求解:選哪些物品放入揹包,可使得這些物品的價值最大,而且體積總和不超過揹包容量。優化
對比一下徹底揹包,其實只是多了一個限制條件,徹底揹包問題中,物品能夠選擇任意多件,只要你裝得下,裝多少件都行。3d
但多重揹包就不同了,每種物品都有指定的數量限制,因此不是你想裝,就能一直裝的。code
舉個栗子:有A、B、C三種物品,相應的數量、價格和佔用空間以下圖:blog
跟徹底揹包同樣,貪心算法在這裏也不適用,我就不重複說明了,你們能夠回到上一篇中看看說明。遞歸
仍是用以前的套路,咱們先來用遞歸把這個問題解決一次。class
用ks(i,t)表示前i種物品放入一個容量爲t的揹包得到的最大價值,那麼對於第i種物品,咱們有k種選擇,0 <= k <= M[i] && 0 <= k * V[i] <= t,便可以選擇0、一、2...M[i]個第i種物品,因此遞推表達式爲:原理
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= t)
同時,ks(0,t)=0;ks(i,0)=0;
對比一下徹底揹包的遞推關係式:
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)
簡直一毛同樣,只是k多了一個限制條件而已。
使用上面的栗子,咱們能夠先寫出遞歸解法:
public static class MultiKnapsack { private static int[] P={0,2,3,4}; private static int[] V={0,3,4,5}; private static int[] M={0,4,3,2}; private static int T = 15; @Test public void soleve1() { int result = ks(P.length - 1,T); 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 <= M[i] && 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/M分別添加了一個元素0,是爲了減小越界判斷而作的簡單處理,運行以下:
最大價值爲:11
對比一下徹底揹包中的遞歸解法:
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; }
僅僅多了一個判斷條件而已,因此只要弄懂了徹底揹包,多重揹包就不值一提了。
最優化原理和無後效性的證實跟多重揹包基本一致,因此就不重複證實了。
參考徹底揹包的動態規劃解法,就很容易寫出多重揹包的動態規劃解法。
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= t)
public static class MultiKnapsack { private static int[] P={0,2,3,4}; private static int[] V={0,3,4,5}; private static int[] M={0,4,3,2}; private static int T = 15; private Integer[][] results = new Integer[P.length + 1][T + 1]; @Test public void solve2() { int result = ks2(P.length - 1,T); 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 <= M[i] && 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、M額外添加的元素0去掉。
除了k的限制不同以外,其餘地方跟徹底揹包的解法徹底一致:
public static class MultiKnapsack { private static int[] P={2,3,4}; private static int[] V={3,4,5}; private static int[] M={4,3,2}; private static int T = 15; 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 <= M[i] && 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 MultiKnapsack { private static int[] P={2,3,4}; private static int[] V={3,4,5}; private static int[] M={4,3,2}; private static int T = 15; 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++){ // 考慮第m個物品 // 分兩種狀況 // 1: M[m] * V[m] > T 則能夠當作徹底揹包問題來處理 if (M[m] * V[m] >= T) { for (int n = V[m]; n <= t ; n++) { newResults[n] = Math.max(newResults[n], newResults[n - V[m]] + P[m]); } } else { // 2: M[m] * V[m] < T 則須要在 newResults[n-V[m]*k] + P[m] * k 中找到最大值,0 <= k <= M[m] for (int n = V[m]; n <= t ; n++) { int k = 1; while (k < M[m] && n > V[m] * k ){ newResults[n] = Math.max(newResults[n], newResults[n - V[m] * k] + P[m] * k); k++; } } } // 能夠在這裏輸出中間結果 System.out.println(JSON.toJSONString(newResults)); } return newResults[newResults.length - 1]; } }
輸出以下:
[0,0,0,0,2,2,2,4,4,4,6,6,6,8,8,8] [0,0,0,0,2,3,3,4,5,6,6,7,8,9,9,10] [0,0,0,0,2,3,4,4,5,6,7,8,8,9,10,11] 11
這裏有一個較大的不一樣點,在第二層循環中,須要分兩種狀況考慮,若是 M[m] * V[m] >= T ,那麼第m個物品就能夠當作徹底揹包問題來考慮,而若是 M[m] * V[m] < T,則每次選擇時,須要從 newResults[n-V[m]*k] + P[m] * k(0 <= k <= M[m])中找到最大值。
代碼很簡單,但要理解卻並不容易,爲了加深理解,再畫一張圖:
多重揹包問題一樣也能夠轉化成01揹包問題來求解,由於第i件物品最多選 M[i] 件,因而能夠把第i種物品轉化爲M[i]件體積和價值相同的物品,而後再來求解這個01揹包問題。
多重揹包問題跟徹底揹包簡直一模一樣,僅僅是比徹底揹包多一個限制條件而已,若是你回過頭去看看前一篇文章,就會發現這篇文章簡直就是抄襲。。
關於多重揹包問題的解析到此就結束了,三個經典的揹包問題到這裏就告一段落了。
若是有疑問或者有什麼想法,也歡迎關注個人公衆號進行留言交流: