此篇博客主要講常見的三種揹包問題,0-1揹包,徹底揹包,多重揹包。爲了加深理解,每一個問題都有leetcode實例。java
有N件物品和一個容量爲V 的揹包。放入第i件物品耗費的空間是Ci,獲得 的價值是Wi。求解將哪些物品裝入揹包可以使價值總和最大。算法
這是最基礎的揹包問題,特色是:每種物品僅有一件,能夠選擇放或不 放。 用子問題定義狀態:即F[i,v]表示前i件物品恰放入一個容量爲v的揹包能夠 得到的最大價值。則其狀態轉移方程即是:數組
F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi}
bash
這個方程很是重要,基本上全部跟揹包相關的問題的方程都是由它衍生 出來的。因此有必要將它詳細解釋一下:「將前i件物品放入容量爲v的揹包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就能夠轉化 爲一個只和前i−1件物品相關的問題。若是不放第i件物品,那麼問題就轉化 爲「前i−1件物品放入容量爲v的揹包中」,價值爲F[i−1,v];若是放第i件物 品,那麼問題就轉化爲「前i−1件物品放入剩下的容量爲v −Ci的揹包中」, 此時能得到的最大價值就是F[i−1,v −Ci]再加上經過放入第i件物品得到的價 值Wi。 僞代碼以下:函數
F[0,0..V ] = 0
for i = 1 to N
for v = Ci to V
F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi}
複製代碼
給定一個只包含正整數的非空數組。是否能夠將這個數組分割成兩個子集,使得兩個子集的元素和相等。
注意:
每一個數組中的元素不會超過 100
數組的大小不會超過 200
示例 1:
輸入: [1, 5, 11, 5]
輸出: true
解釋: 數組能夠分割成 [1, 5, 5] 和 [11].
複製代碼
0-1 揹包問題也是最基礎的揹包問題,它的特色是:待挑選的物品有且僅有一個,能夠選擇也能夠不選擇。下面咱們定義狀態,不妨就用問題的問法定義狀態試試看。學習
dp[i][j]:表示從數組的 [0, i] 這個子區間內挑選一些正整數,每一個數只能用一次,使得這些數的和等於 j。優化
根據咱們學習的 0-1 揹包問題的狀態轉移推導過程,新來一個數,例如是 nums[i],根據這個數可能選擇也可能不被選擇:ui
若是不選擇 nums[i],在 [0, i - 1] 這個子區間內已經有一部分元素,使得它們的和爲 j ,那麼 dp[i][j] = true;spa
若是選擇 nums[i],在 [0, i - 1] 這個子區間內就得找到一部分元素,使得它們的和爲 j - nums[i] ,我既然這樣寫出來了,你就應該知道,這裏討論的前提條件是 nums[i] <= j。 以上兩者成立一條都行。因而獲得狀態轉移方程是:.net
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]], (nums[i] <= j)
因而按照 0-1 揹包問題的模板,咱們不難寫出如下代碼。
參考代碼:
public class Solution {
public boolean canPartition(int[] nums) {
int size = nums.length;
int s = 0;
for (int num : nums) {
s += num;
}
// 特判 2:若是是奇數,就不符合要求
if ((s & 1) == 1) {
return false;
}
int target = s / 2;
// 建立二維狀態數組,行:物品索引,列:容量
boolean[][] dp = new boolean[size][target + 1];
// 先寫第 1 行
for (int i = 1; i < target + 1; i++) {
if (nums[0] == i) {
dp[0][i] = true;
}
}
for (int i = 1; i < size; i++) {
for (int j = 0; j < target + 1; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= nums[i]) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[size - 1][target];
}
}
複製代碼
這也是0-1揹包問題的常規解法。
根據咱們學習過的 0-1 揹包問題的經驗,在填 dp 數組的時候,從第 2 行開始,每一行都參考了前一行的值,所以狀態數組從能夠從二維降到一維,從而減小空間複雜度。
注意:從第 2 行開始,每一行都參考了前一行的當前位置的值,而且還參考了前一行的小於當前位置的值。
public class Solution {
public boolean canPartition(int[] nums) {
int size = nums.length;
int s = 0;
for (int num : nums) {
s += num;
}
if ((s & 1) == 1) {
return false;
}
int target = s / 2;
// 從第 2 行之後,當前行的結果參考了上一行的結果,所以使用一維數組定義狀態就能夠了
boolean[] dp = new boolean[target + 1];
// 先寫第 1 行,看看第 1 個數是否是可以恰好填滿容量爲 target
for (int j = 1; j < target + 1; j++) {
if (nums[0] == j) {
dp[j] = true;
// 若是等於,後面就不用作判斷了,由於 j 會愈來愈大,確定不等於 nums[0]
break;
}
}
// 注意:由於後面的參考了前面的,咱們從後向前填寫
for (int i = 1; i < size; i++) {
// 後面的容量愈來愈小,所以沒有必要再判斷了,退出當前循環
for (int j = target; j >= 0 && j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
}
複製代碼
有N種物品和一個容量爲V 的揹包,每種物品都有無限件可用。放入第i種 物品的耗費的空間是Ci,獲得的價值是Wi。求解:將哪些物品裝入揹包,可以使 這些物品的耗費的空間總和不超過揹包容量,且價值總和最大。
這個問題很是相似於01揹包問題,所不一樣的是每種物品有無限件。也就是從 每種物品的角度考慮,與它相關的策略已並不是取或不取兩種,而是有取0件、 取1件、取2件……直至取⌊V /Ci⌋件等不少種。若是仍然按照解01揹包時的思路,令F[i,v]表示前i種物品恰放入一個容量 爲v的揹包的最大權值。仍然能夠按照每種物品不一樣的策略寫出狀態轉移方 程,像這樣: F[i,v] = max{F[i−1,v−kCi] + kWi |0 ≤ kCi ≤ v}
這跟01揹包問題同樣有O(V N)個狀態須要求解,但求解每一個狀態的時 間已經不是常數了,求解狀態F[i,v]的時間是O( v Ci ),總的複雜度能夠認爲 是O(NV Σ V Ci ),是比較大的。 將01揹包問題的基本思路加以改進,獲得了這樣一個清晰的方法。這說明01 揹包問題的方程的確是很重要,能夠推及其它類型的揹包問題。但咱們仍是要 試圖改進這個複雜度。
給定不一樣面額的硬幣和一個總金額。寫出函數來計算能夠湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。
示例 1:
輸入: amount = 5, coins = [1, 2, 5]
輸出: 4
解釋: 有四種方式能夠湊成總金額:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
複製代碼
此題是個典型的徹底揹包問題,思路: dp[i][j]
表示 :用coins的前i個元素,加出j有多少種方法 用coins的前i個元素,加出j有多少種方法
參考代碼:
public int change(int amount, int[] coins) {
if (amount < 0 || coins == null || coins.length == 0) {
return amount == 0 ? 1 : 0;
}
//dp[i][j]: [0~i]中加出j,有多少種方法
int[][] dp = new int[coins.length][amount + 1];
dp[0][0] = 1;
for (int j = 1; j < amount + 1; ++j) {
dp[0][j] = j % coins[0] == 0 ? 1 : 0;
}
for (int i = 1; i < coins.length; ++i) {
dp[i][0] = 1;
for (int j = 1; j < amount + 1; ++j) {
dp[i][j] = dp[i - 1][j] + (j - coins[i] >= 0 ? dp[i][j - coins[i]] : 0);
}
}
return dp[coins.length - 1][amount];
}
複製代碼
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int i = 1; i < coins.length + 1; ++i) {
for (int j = coins[i - 1]; j < amount + 1; ++j) {
dp[j] += dp[j - coins[i - 1]];
}
}
return dp[amount];
}
複製代碼
徹底揹包問題的解決方法大致和0-1揹包問題相似。
有N種物品和一個容量爲V 的揹包。第i種物品最多有Mi件可用,每件耗費 的空間是Ci,價值是Wi。求解將哪些物品裝入揹包可以使這些物品的耗費的空間 總和不超過揹包容量,且價值總和最大。
這題目和徹底揹包問題很相似。基本的方程只需將徹底揹包問題的方程略 微一改便可。 由於對於第i種物品有Mi+1種策略:取0件,取1件……取Mi件。令F[i,v]表示前i種物品恰放入一個容量爲v的揹包的最大價值,則有狀態轉移方程: F[i,v] = max{F[i−1,v−k∗Ci] + k∗Wi |0 ≤ k ≤ Mi} 複雜度是O(V ΣMi)。
另外一種好想好寫的基本方法是轉化爲01揹包求解:把第i種物品換成Mi件01 揹包中的物品,則獲得了物品數爲ΣMi的01揹包問題。直接求解之,複雜度仍 然是O(V ΣMi)。 可是咱們指望將它轉化爲01揹包問題以後,可以像徹底揹包同樣下降複雜 度。 仍然考慮二進制的思想,咱們考慮把第i種物品換成若干件物品,使得原問 題中第i種物品可取的每種策略——取0...Mi件——均能等價於取若干件代換之後的物品。另外,取超過Mi件的策略必不能出現。 方法是:將第i種物品分紅若干件01揹包中的物品,其中每件物品有一個系 數。 這件物品的費用和價值均是原來的費用和價值乘以這個係數。令這些係數 分別爲1,2,22 ...2k−1,Mi −2k + 1,且k是知足Mi −2k + 1 > 0的最大整數。例 如,若是Mi爲13,則相應的k = 3,這種最多取13件的物品應被分紅係數分別 爲1,2,4,6的四件物品。 分紅的這幾件物品的係數和爲Mi,代表不可能取多於Mi件的第i種物品。另 外這種方法也能保證對於0...Mi間的每個整數,都可以用若干個係數的和表 示。這裏算法正確性的證實能夠分0...2k−1和2k ...Mi兩段來分別討論得出, 但願讀者本身思考嘗試一下。 這樣就將第i種物品分紅了O(logMi)種物品,將原問題轉化爲了複雜度 爲O(V ΣlogMi)的01揹包問題,是很大的改進。 下面給出O(logM)時間處理一件多重揹包中物品的過程:
def MultiplePack(F,C,W,M)
if C ·M ≥ V
CompletePack(F,C,W)
return
k := 1
while k < M
ZeroOnePack(kC,kW)
M := M −k
k := 2k
ZeroOnePack(C ·M,W ·M)
複製代碼
但願你仔細體會這個僞代碼,若是不太理解的話,不妨翻譯成程序代碼之後, 單步執行幾回,或者頭腦加紙筆模擬一下,以加深理解。
在計算機界中,咱們老是追求用有限的資源獲取最大的收益。
如今,假設你分別支配着 m 個 0 和 n 個 1。另外,還有一個僅包含 0 和 1 字符串的數組。
你的任務是使用給定的 m 個 0 和 n 個 1 ,找到能拼出存在於數組中的字符串的最大數量。每一個 0 和 1 至多被使用一次。
注意:
給定 0 和 1 的數量都不會超過 100。
給定字符串數組的長度不會超過 600。
示例 1:
輸入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
輸出: 4
解釋: 總共 4 個字符串能夠經過 5 個 0 和 3 個 1 拼出,即 "10","0001","1","0" 。
複製代碼
參考代碼:
public class Solution {
public int findMaxForm(String[] strs, int m, int n) {
if(strs.length==0)
return 0;
//[0:i-1]的字符串物品中,j個0,k個1最多可以構成字符串數量。字符串爲物品,0,1數量爲揹包限制。
//dp[i][j]=max(dp[i][j],dp[i-0數量][j-1數量]+1)
int[][] dp=new int[m+1][n+1];
for(String str: strs){
int zeros=0, ones=0;
//統計該字符串的0,1數量
for(int i=0; i<str.length(); i++){
char c=str.charAt(i);
if( c=='0')
zeros++;
else
ones++;
}
for(int j=m; j>=zeros; j--)
for(int k=n; k>=ones; k--)
dp[j][k]=Math.max(dp[j][k],1+dp[j-zeros][k-ones]);
}
return dp[m][n];
}
}
複製代碼
揹包問題還包括混合三種揹包的問題,分組的揹包問題等。具體均可以由着三種拓展,能夠參考揹包問題九講。