背包問題
如果求組合數就是外層for循環遍歷物品,內層for遍歷背包體積重量等限制
如果求排列數就是外層for遍歷背包體積重量等限制,內層for循環遍歷物品
參考影片:
- https://www.bilibili.com/video/BV1qt411Z7nE?p=1
- ttps://www.bilibili.com/video/BV1qt411Z7nE?p=2
- https://www.bilibili.com/video/BV1Qt411R7v8?p=2
0/1 背包問題
題目:https://www.acwing.com/problem/content/2/
每種物品只有一樣,每種物品只有取和不取兩種選擇
設 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 個物品中,背包容量為 j j j 時的最大價值( 0 ≤ i ≤ N 0 \leq i \leq N 0≤i≤N, 0 ≤ j ≤ V 0 \leq j \leq V 0≤j≤V)
0/1 背包的狀態轉移方程可以表示為:
d p [ i ] [ j ] = { 0 如果 i = 0 或 j = 0 d p [ i − 1 ] [ j ] 如果 i > 0 且 j < v i max ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v i ] + w i ) 如果 i > 0 且 j ≥ v i dp[i][j] = \begin{cases} 0 & \text{ 如果 } i = 0 \text{ 或 } j = 0 \\ dp[i-1][j] & \text{ 如果 } i > 0 \text{ 且 } j < v_i \\ \max(dp[i-1][j], dp[i-1][j-v_i] + w_i) & \text{ 如果 } i > 0 \text{ 且 } j \geq v_i \end{cases} dp[i][j]=⎩ ⎨ ⎧0dp[i−1][j]max(dp[i−1][j],dp[i−1][j−vi]+wi) 如果 i=0 或 j=0 如果 i>0 且 j<vi 如果 i>0 且 j≥vi
- 當 i i i 等於 0 0 0 或 j j j 等於 0 0 0 時,表示沒有物品可選或背包容量為 0 0 0,此時最大價值為 0 0 0
- 當 i i i 大於 0 0 0 且 j j j 小於 v i v_i vi 時,背包容量不足以放入第 i i i 個物品,所以最大價值與前 i − 1 i-1 i−1 個物品時的最大價值相同
- 當 i i i 大於 0 0 0 且 j j j 大於等於第 i i i 個物品的體積 v i v_i vi 時,對於當前的物品 i i i,有放入和不放入背包兩種選擇。我們考慮的是 d p [ i − 1 ] [ j − v i ] dp[\textcolor{orange}{i - 1}][j-v_i] dp[i−1][j−vi] 的最大價值加上當前物品的價值 w i w_i wi,以及 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]。取兩者中的較大值作為 d p [ i ] [ j ] dp[i][j] dp[i][j] 的值
以下代碼:
// 二維 dp
// 時間複雜度: O(N * V)
// 空間複雜度: O(N * V)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N, V;
N = scanner.nextInt();
V = scanner.nextInt();
int[] volume = new int[N + 1];
int[] worth = new int[N + 1];
int[][] dp = new int[N + 1][V + 1];
for(int i = 1; i <= N; ++i) {
volume[i] = scanner.nextInt();
worth[i] = scanner.nextInt();
}
scanner.close();
for(int i = 1; i <= N; ++i) {
// 必須從 0 ~ V,且在迴圈內在進行判斷
for(int j = 0; j <= V; ++j) {
// 在迴圈內才判斷是為了將 dp[i][j] 的內容有延續(就算下方條件不成立,這裡還是必須延續,這樣下一輪如果需要更新最大值才不會錯;下一輪如果不需更新最大值也要繼續延續)
dp[i][j] = dp[i - 1][j];
// 在此判斷,避免 out of bounds
if(j - volume[i] >= 0)
// 找出最大值存入
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - volume[i]] + worth[i]);
}
}
// 為什麼可以直接返回最後一個值
// 首先是因為這是最後一個比較最大值的,上面註解有寫說這是一路延續下來的,所以 dp[N][V] 肯定和 dp[N - 1][V] 比較過大小,同理 dp[N - 1][V] 和 dp[N - 2][V] 比較過大小,所以無需再找 dp[0~N][V] 的最大值,因為上面的巢狀迴圈已處理過,所以保證最後一個一定是最大;再者 dp[N][V] 是最大體積的,也是最後一個在巢狀迴圈中比較的數字,所以一路比下來我們可以說它是最大的。
System.out.println(dp[N][V]);
}
}
// 降維 dp
// 時間複雜度: O(N * V)
// 空間複雜度: O(V)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N, V;
N = scanner.nextInt();
V = scanner.nextInt();
int[] volume = new int[N + 1];
int[] worth = new int[N + 1];
int[] dp = new int[V + 1];
for(int i = 1; i <= N; ++i) {
volume[i] = scanner.nextInt();
worth[i] = scanner.nextInt();
}
scanner.close();
// 這裡能降維的關鍵是在只有 2 個 row 的陣列在跑
for(int i = 1; i <= N; ++i) {
// 這裡 j 必須要倒序的主要原因是因為 dp[j - volume[i]],實際我們要的是上一輪的內容(dp[i - 1][j - volume[i]]),但這裡降維了,所以這裡的 j 要使用倒序來避免被此輪的更新染污 dp[j - volume[i]]
// 至於右側比較大小的 dp[j] 也是上一輪的拿來比較後才進行更新,所以 dp[j] 在這裡的正序倒序皆不影響,只是因為 dp[j - volume[i]] 所以要倒序
// V ~ volume[i],這裡也與二維的不同,二維是必須完整 run 1 ~ V,之後才在迴圈裡判斷;這裡則是因為 dp[j] 已經承襲了上一輪的 dp[j](這也是降維的特點,自動延續),所以可以直接在迴圈中排除不可能(造成陣列 out of bound)的情況
for(int j = V; j >= volume[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - volume[i]] + worth[i]);
}
}
// 為什麼這裡可以直接返回最後一個值呢,其實和二維的思路差不多,但這裡可能有些人會有疑問,我的 dp[V] 在上方的巢狀迴圈是最後一個 row 的第一個數字,難道說最後一個 row 的第一個比完之後,後面比的都沒用嗎?感覺就說不上來的怪啊?
// 但其實這是沒有問題的,你再仔細觀察我們二維的狀態轉移式,再看一維就沒問題,二維時我們的 dp[i][j] 都是從 dp[i - 1][...] 而來,也就是說我們當前 row 的所有值全部都取決於上一輪而非當前輪
// 在最大體積時的值一定會是當前輪最大的(可以體會一下這句話),這是動態規劃保證的特性(在二維時有些人會比較 dp[0~N][V] 的最大值時,其實也用到了這個特性,因為已經確保了最大體積能在當前輪求得最大值,所以這樣去比較求得總體最大值的思路沒錯,但我已經在二維時說了,不用這樣去比較,直接返回最後一個值即可)。
// 融合以下兩個特性,當前輪都是上一輪而來 + 最大體積時一定會是當前輪最大的,那麼"最後一個 row 的第一個數字"是最大值,也是應當返回的值,就相當合理了
System.out.println(dp[V]);
}
}
完全背包問題
題目:https://www.acwing.com/problem/content/description/3/
每種物品有無限多種,在有限體積中求最大價值
{ d p [ i , j ] = max ( d p [ i − 1 , j ] , d p [ i − 1 , j − v ] + w , d p [ i − 1 , j − 2 v ] + 2 w , d p [ i − 1 , j − 3 v ] + 3 w , … ) d p [ i , j − v ] = max ( d p [ i − 1 , j − v ] , d p [ i − 1 , j − 2 v ] + w , d p [ i − 1 , j − 3 v ] + 2 w , … ) \begin{cases} dp[i, \quad j \quad ] = \max(\ dp[i-1, \ j], \ dp[i-1, \ j-v] + w, \ dp[i-1, \ j-2v] + 2w, \ dp[i-1, \ j-3v] + 3w, \ \ldots) \\ dp[i, \ j-v] = \max(\quad\quad\quad\quad\quad \ \ \ dp[i-1, \ j-v], \quad\quad \ dp[i-1, \ j-2v] + \ \ w, \ dp[i-1, \ j-3v] + 2w, \ \ldots) \end{cases} {dp[i,j]=max( dp[i−1, j], dp[i−1, j−v]+w, dp[i−1, j−2v]+2w, dp[i−1, j−3v]+3w, …)dp[i, j−v]=max( dp[i−1, j−v], dp[i−1, j−2v]+ w, dp[i−1, j−3v]+2w, …)
觀察上方兩式(兩者長很像,除去第一個不看之外,其它只是差了一個 w,所以最後補上),可得出如下:
d p [ i , j ] = max ( d p [ i − 1 , j ] , d p [ i − 1 , j − v ] + w , d p [ i − 1 , j − 2 v ] + 2 w , d p [ i − 1 , j − 3 v ] + 3 w , … ) = max ( d p [ i − 1 , j ] , d p [ i , j − v ] + w ) \begin{align*} & dp[i, \quad j \quad ] = \max(\ dp[i-1, \ j], \ dp[i-1, \ j-v] + w, \ dp[i-1, \ j-2v] + 2w, \ dp[i-1, \ j-3v] + 3w, \ \ldots) \\ & \quad\quad\quad\quad\quad = \max(\ dp[i-1, \ j], \quad\quad\quad\quad\quad\quad\quad\quad dp[i, \quad j - v ] + w) \end{align*} dp[i,j]=max( dp[i−1, j], dp[i−1, j−v]+w, dp[i−1, j−2v]+2w, dp[i−1, j−3v]+3w, …)=max( dp[i−1, j],dp[i,j−v]+w)
設 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 個物品中,背包容量為 j j j 時的最大價值( 0 ≤ i ≤ N 0 \leq i \leq N 0≤i≤N, 0 ≤ j ≤ V 0 \leq j \leq V 0≤j≤V)
完全背包的狀態轉移方程可以表示為:
d p [ i ] [ j ] = { 0 如果 i = 0 或 j = 0 d p [ i − 1 ] [ j ] 如果 i > 0 且 j < v i max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v i ] + w i ) 如果 i > 0 且 j ≥ v i dp[i][j] = \begin{cases} 0 & \text{ 如果 } i = 0 \text{ 或 } j = 0 \\ dp[i-1][j] & \text{ 如果 } i > 0 \text{ 且 } j < v_i \\ \max(dp[i-1][j], dp[i][j-v_i] + w_i) & \text{ 如果 } i > 0 \text{ 且 } j \geq v_i \end{cases} dp[i][j]=⎩ ⎨ ⎧0dp[i−1][j]max(dp[i−1][j],dp[i][j−vi]+wi) 如果 i=0 或 j=0 如果 i>0 且 j<vi 如果 i>0 且 j≥vi
- 當 i i i 等於 0 0 0 或 j j j 等於 0 0 0 時,表示沒有物品可選或背包容量為 0 0 0,此時最大價值為 0 0 0
- 當 i i i 大於 0 0 0 且 j j j 小於 v i v_i vi 時,背包容量不足以放入第 i i i 個物品,所以最大價值與前 i − 1 i-1 i−1 個物品時的最大價值相同
- 當 i i i 大於 0 0 0 且 j j j 大於等於第 i i i 個物品的體積 v i v_i vi 時,對於當前的物品 i i i,我們可以選擇將其放入背包中多次,直到背包容量無法再容納它為止。我們考慮的是 d p [ i ] [ j − v i ] dp[\textcolor{orange}i][j-v_i] dp[i][j−vi] 的值加上當前物品的價值 w i w_i wi(只有橘色的地方與 0/1 背包不同),以及 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]。取兩者中的較大值作為 d p [ i ] [ j ] dp[i][j] dp[i][j] 的值
// 二維 dp
// 時間複雜度: O(N*V)
// 空間複雜度: O(N*V)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] volume = new int[N + 1];
int[] worth = new int[N + 1];
int[][] dp = new int[N + 1][V + 1];
for(int i = 1; i <= N; ++i) {
volume[i] = scanner.nextInt();
worth[i] = scanner.nextInt();
}
scanner.close();
for(int i = 1; i <= N; ++i) {
// 為了延續,所以從 1 ~ V,就算 (j - v[i]) < 0 也須將上一輪的值更新至當前輪
for(int j = 1; j <= V; ++j) {
dp[i][j] = dp[i - 1][j];
// 滿足放入條件,放入後比較大小,大的存入
if(j - volume[i] >= 0)
dp[i][j] = Math.max(dp[i][j], dp[i][j - volume[i]] + worth[i]);
}
}
System.out.println(dp[N][V]);
}
}
// 降維 dp
// 時間複雜度: O(N*V)
// 空間複雜度: O(V)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N, V;
N = scanner.nextInt();
V = scanner.nextInt();
int[] volume = new int[N + 1];
int[] worth = new int[N + 1];
int[] dp = new int[V + 1];
for(int i = 1; i <= N; ++i) {
volume[i] = scanner.nextInt();
worth[i] = scanner.nextInt();
}
scanner.close();
for(int i = 1; i <= N; ++i) {
// j 正序 => 每一輪從前方走到後方時,都嘗試在放入此輪的物品 w[i] 後與 dp[j] 比大小(因為是正序,所以後方放入的是前方已經遍歷過的,細品一下,dp[j - v[i]] + w[i] 的 dp[j - v[i] 是不是在此輪的前方才遍歷過,也就是在此輪中不斷嘗試重覆放入找最大,亦即二維的 dp[i][j - v[i]])。在此輪的每一個 j 遍歷的體積中,盡可能都去塞入 w[i] 和 dp[j] 比較一下大小,大的存入 dp[j]
// 在當前輪中,更新每一體積的最大值,在此輪每個 j 中嘗試塞入所有 w[i] 的可能,和 dp[j](這裡的 dp[j] 因為尚未更新,指的就是上一輪的 dp[j],即二維的 dp[i - 1][j]) 比較大小,所以在一輪中可能塞入多個 w[i](如果塞了 w[i] 後都比上一輪大的話)
for(int j = volume[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j], dp[j - volume[i]] + worth[i]);
}
}
System.out.println(dp[V]);
}
}
多重背包
題目:https://www.acwing.com/problem/content/4/
從此題開始除特殊情況外,只保留降維 dp 的寫法
每種物品有 0 ~ s[i] 種可能,每種物品的最大數量皆可能不同,求物品能放入背包的最大價值
多重背包的樣態應該會像這樣,其實就是從 0/1 背包擴展出來,只是每件物品有 s[i] 個,即每件物品有從 0 ~ s[i] 共 s[i] + 1 選擇(選或不選,都不選就是該物品選 0 樣)
for(int i = 1; i <= N; ++i) {
for(int j = V; j >= v[i]; --j) {
// dp[j] 是上一輪的 dp[j],就是都不選,後面是選 1 種 ~ 選 s[i] 種
dp[j] = max(dp[j], dp[j - v[i]] + w[i], dp[j - 2 * v[i]] + 2 * w[i] + ... dp[j - s[i] * v[i]] + s[i] * w[i])
}
}
降維後的狀態轉移式(此處省略了遍歷物品,但記得代碼要寫遍歷物品)
d p [ j ] = 0 如果 j = 0 d p [ j ] = max ( d p [ j ] , d p [ j − k ⋅ v [ i ] ] + k ⋅ w [ i ] ) 如果 j ≥ k ⋅ v [ i ] ,其中 1 ≤ k ≤ s [ i ] \begin{align*} dp[j] &= 0 \quad && \text{如果 } j = 0 \\ dp[j] &= \max(dp[j], dp[j-k \cdot v[i]] + k \cdot w[i]) \quad && \text{如果 } j \geq k \cdot v[i] \text{,其中 } 1 \leq k \leq s[i] \end{align*} dp[j]dp[j]=0=max(dp[j],dp[j−k⋅v[i]]+k⋅w[i])如果 j=0如果 j≥k⋅v[i],其中 1≤k≤s[i]
// 時間複雜度:O(N * V * K),K 為 s[i] 可能範圍的最大值
// 空間複雜度:O(V)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
int[] s = new int[N + 1];
int[] dp = new int[V + 1];
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
s[i] = scanner.nextInt();
}
for(int i = 1; i <= N; ++i) {
// 0/1 背包的思路(選或不選)再加入 k 個該物品的遍歷可能,也就是該物品有 0 ~ k 個選擇
for(int j = V; j >= v[i]; --j) {
for(int k = 1; k <= s[i] && (j - k * v[i]) >= 0; ++k) {
dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
}
System.out.println(dp[V]);
}
}
多重背包 二進制優化
題目:https://www.acwing.com/problem/content/description/5/
當數據量大時,必須要做優化,這裡使用二進制優化
- 如果我們要遍歷 7,使用 3 個二進位數字即可表示
- 如果我們要遍歷 13,使用 4 個二進位數字即可表示,多餘的放進最高位即可
13 = 1 + 2 + 4 + 6
0 ~ 7 使用 1, 2, 4 表示
8 使用 2 + 6
9 使用 1 + 2 + 6
10 使用 4 + 6
11 使用 1 + 4 + 6
12 使用 2 + 4 + 6
13 使用 1 + 2 + 4 + 6
10 = 1 + 2 + 4 + 3
0 ~ 7 使用 1, 2, 4 表示
8 使用 1 + 4 + 3
9 使用 2 + 4 + 3
10 使用 1 + 2 + 4 + 3
如果數字不滿足 2^N - 1,將剩餘的數字放入最高位即可用 log(2^N) 的位數列出所有可能
// 二進制優化
// 時間複雜度:O(N * log(K) * V),K 為 s[i] 可能範圍的最大值
// 空間複雜度:O(Max(V, N * log(K)))
import java.util.*;
public class Main {
// 定義靜態類,方便等下放入 v 和 w
public static class Goods {
private int v;
private int w;
public Goods(int v, int w) {
this.v = v;
this.w = w;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
int[] s = new int[N + 1];
int[] dp = new int[V + 1];
ArrayList<Goods> GoodsArr = new ArrayList<>();
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
s[i] = scanner.nextInt();
// 將物品數量由十進制轉成二進制存儲(這裡會將所有物品轉換後存入 GoodsArr)
for(int k = 1; k <= s[i]; k *= 2) {
s[i] -= k;
GoodsArr.add(new Goods(k * v[i], k * w[i]));
}
// 如果還有剩餘的數字(代表 s[i] 不滿足 2^N - 1),放入最高位
if(s[i] != 0)
GoodsArr.add(new Goods(s[i] * v[i], s[i] * w[i]));
}
// 原本 0/1 背包是遍歷第 i 件物品,此時已將所有物品都轉化為二進制形式存放(等於遍歷了每種物品的所有可能),所以外層迴圈直接遍歷轉換過的二進制即可
// i 遍歷的是 GoodsArr 長度,為 N * log(K) 次
for(int i = 0; i < GoodsArr.size(); ++i) {
for(int j = V; j >= GoodsArr.get(i).v; --j) {
dp[j] = Math.max(dp[j], dp[j - GoodsArr.get(i).v] + GoodsArr.get(i).w);
}
}
System.out.println(dp[V]);
}
}
混合背包
題目:https://www.acwing.com/problem/content/7/
0/1 背包、完全背包與多重背包的混合體
// 時間複雜度:O(N * log(K) * V),K 為 s[i] 可能範圍的最大值
// 空間複雜度:O(Max(V, N * log(K)))
import java.util.*;
public class Main {
public static class Goods {
int v, w;
int kind;
public Goods(int kind, int v, int w) {
this.kind = kind;
this.v = v;
this.w = w;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
int[] s = new int[N + 1];
int[] dp = new int[V + 1];
ArrayList<Goods> GoodsArr = new ArrayList<>();
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
s[i] = scanner.nextInt();
// 如果是 0/1 背包
if(s[i] == -1)
GoodsArr.add(new Goods(-1, v[i], w[i]));
// 如果是完全背包
else if(s[i] == 0)
GoodsArr.add(new Goods(0, v[i], w[i]));
// 如果是多重背包,做特別處理
else if(s[i] > 0) {
// 多重背包轉成 0/1 背包的二進制優化
// 轉換後以 0/1 背包存儲
for(int k = 1; k <= s[i]; k *= 2) {
s[i] -= k;
GoodsArr.add(new Goods(-1, k * v[i], k * w[i]));
}
if(s[i] != 0)
GoodsArr.add(new Goods(-1, s[i] * v[i], s[i] * w[i]));
}
}
for(int i = 0; i < GoodsArr.size(); ++i) {
int cur_v = GoodsArr.get(i).v;
int cur_w = GoodsArr.get(i).w;
// 0/1 背包(包含從多重背包轉進來的)
if(GoodsArr.get(i).kind < 0) {
for(int j = V; j >= cur_v; --j) {
dp[j] = Math.max(dp[j], dp[j - cur_v] + cur_w);
}
}
// 完全背包
else {
for(int j = cur_v; j <= V; ++j) {
dp[j] = Math.max(dp[j], dp[j - cur_v] + cur_w);
}
}
}
System.out.println(dp[V]);
}
}
二維費用的背包問題
題目:https://www.acwing.com/problem/content/8/
限制從一個維度擴展到兩個維度,所以 dp 也相應擴展一個維度,若是 0/1 背包就用 0/1 背包的思維去解即可
// 時間複雜度:O(N * V * M)
// 空間複雜度:O(V * M)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int M = scanner.nextInt();
int[] v = new int[N + 1];
int[] m = new int[N + 1];
int[] w = new int[N + 1];
int[][] dp = new int[V + 1][M + 1];
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
m[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
for(int i = 1; i <= N; ++i) {
int cur_v = v[i];
int cur_m = m[i];
for(int j = V; j >= cur_v; --j) {
for(int k = M; k >= cur_m; --k) {
dp[j][k] = Math.max(dp[j][k], dp[j - cur_v][k - cur_m] + w[i]);
}
}
}
System.out.println(dp[V][M]);
}
}
分組背包
題目:https://www.acwing.com/problem/content/9/
一組之中至多只能選特定的一個,也可以不選,但一組不能選超過一個物品
多重背包是分組背包的一個特殊情況,將多重背包物品的個數看成是分組背包的組數
多重背包對於該物品的選擇數看做是分組背包一組內的選擇(一組代表一個物品),[這一組中從都不選,選一個,到該物品選擇 s[i] 個],一組裡只能挑出一個就恰好是多重背包中選擇該物品的個數
再形象點說,把多重背包的物品的個數看成是分組背包組內的選擇,在這個分組背包中,這一組對該物品有 [都不打包,打包一件,打包二件…打包 s[i] 件],然後分組背包的意思是說在一組中我只能選一個,那不就正好是多重背包中要求的該物品選擇的個數嗎?
本質上是一種 0/1 背包,選與不選
多重背包是分組背包的一種特例,但多重背包有可以優化的方式,分組背包範圍則更廣泛,目前還沒有優化的方式
for(int i = 1; i <= N; ++i) {
for(int j = V; j >= v[i]; --j) {
dp[j] = max(dp[j], dp[j - v[1]] + w[1], dp[j - v[2]] + w[2] + ... + dp[j - v[s]] + w[s]);
}
}
降維後的狀態轉移式(此處省略了遍歷物品,但記得代碼要寫遍歷物品)
d p [ j ] = 0 如果 j = 0 d p [ j ] = max ( d p [ j ] , d p [ j − v [ k ] ] + w [ k ] ) 如果 j ≥ v [ k ] ,其中 1 ≤ k ≤ s \begin{align*} dp[j] &= 0 \quad && \text{如果 } j = 0 \\ dp[j] &= \max(dp[j], dp[j-v[k]] + w[k]) \quad && \text{如果 } j \geq v[k] \text{,其中 } 1 \leq k \leq s \end{align*} dp[j]dp[j]=0=max(dp[j],dp[j−v[k]]+w[k])如果 j=0如果 j≥v[k],其中 1≤k≤s
// 時間複雜度:O(N * V * S)
// 空間複雜度:O(V)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] dp = new int[V + 1];
for(int i = 1; i <= N; ++i) {
int S = scanner.nextInt();
int[] v = new int[S + 1];
int[] w = new int[S + 1];
for(int k = 1; k <= S; ++k) {
v[k] = scanner.nextInt();
w[k] = scanner.nextInt();
}
for(int j = V; j >= 0; --j) {
for(int k = 1; k <= S; ++k) {
// 要記得確保 j - v[k] 不越界
if(j - v[k] >= 0)
dp[j] = Math.max(dp[j], dp[j - v[k]] + w[k]);
}
}
}
System.out.println(dp[V]);
}
}
有依賴的背包問題
困難題(為面試刷題的可以跳過)
此題是在選擇物品前需要先選擇其依賴的物品
題目:https://www.acwing.com/problem/content/description/10/
// 時間複雜度:O(N^2 * V)
// 空間複雜度:O(N * V)
import java.util.*;
public class Main {
public static void dfs(int x, ArrayList<Integer>[] dep_node, int[][] dp, int[] v, int[] w, int N, int V) {
// 所有選到 x 的都需放入 w[x],確保有空間放入,所以只放入容量大於 v[x] 的可能
// 因為不放入就不能遍歷其的依賴節點(題意是 x 節點走過才能走依賴 x 節點的節點)
for(int j = v[x]; j <= V; ++j)
dp[x][j] = w[x];
// 遍歷所有依賴節點
for(int i = 0; i < dep_node[x].size(); ++i) {
// 找出依賴於 x 節點的下一個值,要遍歷所有依賴 x 節點的節點
int y = dep_node[x].get(i);
// 樹狀 dp,為了遍歷所有依賴 x 節點的可能
dfs(y, dep_node, dp, v, w, N, V);
// 本質上是一個 0/1 背包,每種物品只有選與不選
// 要保證 x 這個節點能放入 w[i]
for(int j = V; j >= v[x]; --j) {
// 遍歷下一個依賴節點最多能放入的容量 [ 1, j - v[x] ]
// k = 0 就是原先上一輪的 dp[x][j]
for(int k = 1; k <= j - v[x]; ++k) {
// 為此節點(x)留下 v[x] 的空間,這樣下一個依賴節點(y)最多能放入的容量為 j - v[x]
dp[x][j] = Math.max(dp[x][j], dp[x][j - k] + dp[y][k]);
}
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
// dp[x][i] 表示以 x 為子樹的物品,在容量不超過 i 時的最大價值
int[][] dp = new int[N + 1][V + 1];
// dep_node[x][i] 表示所有依賴 x 的物品節點,在容量不超過 i 時的最大價值
ArrayList<Integer>[] dep_node = new ArrayList[N + 1];
for(int i = 1; i <= N; ++i)
dep_node[i] = new ArrayList<>();
int root = -1;
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
int p = scanner.nextInt();
if(p == -1) {
root = i;
}
else
dep_node[p].add(i);
}
// 先塞入 root,找符合條件的最大價值
dfs(root, dep_node, dp, v, w, N, V);
// 印出由 root 節點開始遍歷的最大容量就是答案,為什麼 root 物品在最大容量中的會是最大的呢,因為 dfs 中遍歷求最大值是後序操作的,也就意謂著當你先塞入 root,但 root 實際上最後才會被執行。
// 遞歸它就是個棧,先壓 root,再壓與 root 依賴的節點(y),再壓與該節點(y)依賴的節點…,先進後出,結合棧與後序位置才遍歷的特性,就可以知道 root 雖然先進入,但是最後才執行遍歷操作。
// 最後才遍歷的意義就是底下節點都遍歷好了,回到我們最開始就說過的邏輯,那我們打印最後一輪中容量最大的就會是答案
System.out.println(dp[root][V]);
}
}
背包問題求方案數
題目:https://www.acwing.com/problem/content/11/
此題要求 0/1 背包的最大值方案總數
0/1 背包的狀態轉移方程可以表示為:
dp[i][j] 是考慮前 i 個物品,當前容量不超過 j 的最大價值
d p [ i ] [ j ] = { 0 如果 i = 0 或 j = 0 d p [ i − 1 ] [ j ] 如果 i > 0 且 j < v i max ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v i ] + w i ) 如果 i > 0 且 j ≥ v i dp[i][j] = \begin{cases} 0 & \text{ 如果 } i = 0 \text{ 或 } j = 0 \\ dp[i-1][j] & \text{ 如果 } i > 0 \text{ 且 } j < v_i \\ \max(dp[i-1][j], dp[i-1][j-v_i] + w_i) & \text{ 如果 } i > 0 \text{ 且 } j \geq v_i \end{cases} dp[i][j]=⎩ ⎨ ⎧0dp[i−1][j]max(dp[i−1][j],dp[i−1][j−vi]+wi) 如果 i=0 或 j=0 如果 i>0 且 j<vi 如果 i>0 且 j≥vi
路徑跟蹤
pathNum[i][j] 是考慮前 i 個物品,當前使用容量恰好為 j,且價值最大的方案數
p a t h N u m [ i ] [ j ] = { 1 如果 i = 0 且 j = 0 p a t h N u m [ i − 1 ] [ j ] + p a t h N u m [ i − 1 ] [ j − v ] 如果 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] 且 d p [ i ] [ j ] = d p [ i − 1 ] [ j − v i ] + w i p a t h N u m [ i − 1 ] [ j ] 如果 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] 且 d p [ i ] [ j ] ≠ d p [ i − 1 ] [ j − v i ] + w i p a t h N u m [ i − 1 ] [ j − v ] 如果 d p [ i ] [ j ] ≠ d p [ i − 1 ] [ j ] 且 d p [ i ] [ j ] = d p [ i − 1 ] [ j − v i ] + w i pathNum[i][j] = \begin{cases} 1 & \text{ 如果 } i = 0 \text{ 且 } j = 0 \\ pathNum[i - 1][j] + pathNum[i - 1][j - v] & \text{ 如果 } dp[i][j] = dp[i - 1][j] \text{ 且 } dp[i][j] = dp[i - 1][j - v_i] + w_i \\ pathNum[i-1][j] & \text{ 如果 } dp[i][j] = dp[i - 1][j] \text{ 且 } dp[i][j] \neq dp[i - 1][j - v_i] + w_i \\ pathNum[i - 1][j - v] & \text{ 如果 } dp[i][j] \neq dp[i - 1][j] \text{ 且 } dp[i][j] = dp[i - 1][j - v_i] + w_i \\ \end{cases} pathNum[i][j]=⎩ ⎨ ⎧1pathNum[i−1][j]+pathNum[i−1][j−v]pathNum[i−1][j]pathNum[i−1][j−v] 如果 i=0 且 j=0 如果 dp[i][j]=dp[i−1][j] 且 dp[i][j]=dp[i−1][j−vi]+wi 如果 dp[i][j]=dp[i−1][j] 且 dp[i][j]=dp[i−1][j−vi]+wi 如果 dp[i][j]=dp[i−1][j] 且 dp[i][j]=dp[i−1][j−vi]+wi
// 二維 dp
// 時間複雜度:O(N * V)
// 時間複雜度:O(N * V)
import java.util.*;
public class Main {
final static int mod = 1000000007;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
int[][] dp = new int[N + 1][V + 1];
int[][] pathNum = new int[N + 1][V + 1];
pathNum[0][0] = 1;
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
for(int i = 1; i <= N; ++i) {
for(int j = V; j >= 0; --j) {
dp[i][j] = dp[i - 1][j];
if(j >= v[i])
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
if(j >= v[i] && dp[i - 1][j] == dp[i - 1][j - v[i]] + w[i])
pathNum[i][j] = (pathNum[i - 1][j] + pathNum[i - 1][j - v[i]]) % mod;
else if(dp[i][j] == dp[i - 1][j])
pathNum[i][j] = pathNum[i - 1][j] % mod;
else
pathNum[i][j] = pathNum[i - 1][j - v[i]] % mod;
}
}
int res = 0;
for(int j = 0; j <= V; ++j) {
if(dp[N][V] == dp[N][j])
res += pathNum[N][j] % mod;
}
System.out.println(res);
}
}
// 降維 dp
// 時間複雜度:O(N * V)
// 時間複雜度:O(V)
import java.util.*;
public class Main {
final static int mod = 1000000007;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
int[] dp = new int[V + 1];
int[] pathNum = new int[V + 1];
pathNum[0] = 1;
for(int i = 1; i <= N; ++i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
for(int i = 1; i <= N; ++i) {
for(int j = V; j >= v[i]; --j) {
int tmp = Math.max(dp[j], dp[j - v[i]] + w[i]);
if(dp[j] == dp[j - v[i]] + w[i])
pathNum[j] = (pathNum[j] + pathNum[j - v[i]]) % mod;
else if(tmp == dp[j])
pathNum[j] = pathNum[j] % mod;
else
pathNum[j] = pathNum[j - v[i]] % mod;
dp[j] = tmp;
}
}
int res = 0;
for(int j = 0; j <= V; ++j) {
if(dp[V] == dp[j])
res += pathNum[j] % mod;
}
System.out.println(res);
}
}
背包問題求具體方案
題目:https://www.acwing.com/problem/content/description/12/
實際上這題是要找出最大值的具體選擇,並且依編號升序排列
此題需要求具體方案的路徑,所以不能降維
// 二維 dp
// 時間複雜度:O(N * V)
// 空間複雜度:O(N * V)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] v = new int[N + 1];
int[] w = new int[N + 1];
int[][] dp = new int[N + 1][V + 1];
// 答案要按編號順序排列,所以先放入最後一個物品,再依序放入直至第一個,也就是倒序放入物品
// 原本的 1 變為 N,原本的 N 變為 1
for(int i = N; i >= 1; --i) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
// 雖然 i 遍歷是正序的,先遍歷 1,再繼續遍歷 2 , … 直至遍歷至 N
// 實際遍歷是倒序的,先放入編號 N,再嘗試放入 N - 1,… 直至嘗試放入 1
for(int i = 1; i <= N; ++i) {
// 0/1 背包
for(int j = 0; j <= V; ++j) {
dp[i][j] = dp[i - 1][j];
if(j - v[i] >= 0)
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
}
}
// 記錄當前的背包容量
int cur_v = V;
// 從結果(dp[N][M] 是最大價值)反推其路徑,雖說是反推但其實我們原本的順序是顛倒的,所以 i 雖然是倒序,但編號是正序遍歷
for(int i = N; i >= 1; --i) {
// 滿足條件,代表 dp[i][cur_v] 是其推導過來,所以我們要往回走
if(cur_v >= v[i] && dp[i][cur_v] == dp[i - 1][cur_v - v[i]] + w[i]) {
cur_v -= v[i];
// 打印出來時,注意 i 是倒的,但編號是正的,把 i 轉為編號(當 i 是 N 時,編號為 1)
System.out.printf(N - i + 1 + " ");
}
}
}
}
恰好等於背包體積求最大最小值(番外篇)
當背包要求恰好裝滿時的,就不能再初始化為 0,而是應該初始化為反向的極值,也就是如果要你求最大值,那就應該設為負無窮;求最小值,就設成正無窮。但你必須設置 dp[0] 為 0
為什麼這樣就會是恰好裝滿呢,當我們不需恰好裝滿時將所有體積的 dp 路徑都初始為 0,這就會造成不恰好裝滿的情況(例如 dp[1] = 0,即當體積為 1 的時候,價值為 0,就是沒裝滿的情況,往後迭代如果用到體積 1 時就可能讓沒裝滿的情況一路往後延續),我們只要將初始值設為都是恰好裝滿,不恰好裝滿的體積設一個相反極值讓它被之後我們 dp[0] = 0 推導出來的恰好裝滿給輕易取代掉即可
拿上面的完全背包一維 dp 舉例,0/1 背包也是同理修改初始值即可
// 降維 dp
// 時間複雜度: O(N * V)
// 空間複雜度: O(V)
import java.util.Scanner;
import.java.util.Arrays;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N, V;
N = scanner.nextInt();
V = scanner.nextInt();
int[] volume = new int[N + 1];
int[] worth = new int[N + 1];
int[] dp = new int[V + 1];
for(int i = 1; i <= N; ++i) {
volume[i] = scanner.nextInt();
worth[i] = scanner.nextInt();
}
// 求最大值,所以設為負無窮
Arrays.fill(dp, Integer.MIN_VALUE)
// 很重要,當體積為 0 時的價值為 0 是合法的(也就是恰好裝滿背包,體積 0 => 沒塞東西 => 價值 0)。藉由設置此才能一路往後推,讓其它經過的 dp 路徑合法
dp[0] = 0;
scanner.close();
for(int i = 1; i <= N; ++i) {
for(int j = volume[i]; j <= V; ++j) {
// 提前判定,如果求最小值, dp 初始為最大值時要防止溢位才需設置此。但這裡無論求最大還最小統一都設置 if 條件,才不會混亂
if(dp[j - volume[i]] == Integer.MIN_VALUE)
dp[j] = Math.max(dp[j], dp[j - volume[i]] + worth[i]);
}
}
if(dp[V] != Integer.Min_VALUE)
System.out.println(dp[V]);
else
System.out.println("背包無法恰好裝滿");
}
}
求最小值的題目
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int T = scanner.nextInt();
for(int k = 0; k < T; ++k) {
int E = scanner.nextInt();
int F = scanner.nextInt();
int V = F - E;
int N = scanner.nextInt();
int[] weight = new int[N + 1];
int[] value = new int[N + 1];
int[] dp = new int[V + 1];
for(int i = 1; i <= N; ++i) {
value[i] = scanner.nextInt();
weight[i] = scanner.nextInt();
}
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for(int i = 1; i <= N; ++i) {
for(int j = weight[i]; j <= V; ++j) {
// 排除可能的溢位錯誤
if(dp[j - weight[i]] != Integer.MAX_VALUE)
dp[j] = Math.min(dp[j], dp[j - weight[i]] + value[i]);
}
}
if(dp[V] == Integer.MAX_VALUE)
System.out.println("This is impossible.");
else
System.out.println("The minimum amount of money in the piggy-bank is " + dp[V] + ".");
}
}
}