背包问题完全指南
背包问题完全指南
背包问题是动态规划中的经典问题,也是面试和竞赛中的高频考点。本文将系统性地介绍各类背包问题的原理、解法和例题。
背包问题分类体系
一、01 背包问题
问题描述
有 N 件物品和一个容量为 V 的背包。第 i 件物品的体积是 w[i],价值是 v[i]。每件物品最多只能选一次,求解将哪些物品装入背包可使价值总和最大。
决策树图解
状态定义
dp[i][j] 表示前 i 件物品放入容量为 j 的背包中的最大价值。
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])dp[i-1][j]:不选第i件物品dp[i-1][j-w[i]] + v[i]:选第i件物品(前提是j >= w[i])
空间优化(一维数组)
由于 dp[i] 只依赖于 dp[i-1],可以用一维数组优化:
for (int i = 1; i <= n; i++) { for (int j = V; j >= w[i]; j--) { // 逆序遍历 dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); }}关键点:逆序遍历是为了保证每个物品只被使用一次。
01 背包遍历方向图解
例题:AcWing 2. 01背包问题
题目描述:有 N 件物品和一个容量是 V 的背包。每件物品只能用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
代码实现:
import java.util.Scanner;
public class Knapsack01 { static final int MAXN = 1005; static int n, V; static int[] w = new int[MAXN]; static int[] v = new int[MAXN]; static int[] dp = new int[MAXN];
public static void main(String[] args) { Scanner sc = new Scanner(System.in); n = sc.nextInt(); V = sc.nextInt();
for (int i = 1; i <= n; i++) { w[i] = sc.nextInt(); v[i] = sc.nextInt(); }
for (int i = 1; i <= n; i++) { for (int j = V; j >= w[i]; j--) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } }
System.out.println(dp[V]); sc.close(); }}二、完全背包问题
问题描述
有 N 种物品和一个容量为 V 的背包。第 i 种物品的体积是 w[i],价值是 v[i]。每种物品有无限件可用,求解将哪些物品装入背包可使价值总和最大。
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i])注意与 01 背包的区别:这里是 dp[i][j-w[i]] 而不是 dp[i-1][j-w[i]],因为物品可以重复选择。
空间优化(一维数组)
for (int i = 1; i <= n; i++) { for (int j = w[i]; j <= V; j++) { // 正序遍历 dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); }}关键点:正序遍历,允许物品被重复使用。
完全背包遍历方向图解
例题:AcWing 3. 完全背包问题
题目描述:有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
代码实现:
import java.util.Scanner;
public class CompleteKnapsack { static final int MAXN = 1005; static int n, V; static int[] w = new int[MAXN]; static int[] v = new int[MAXN]; static int[] dp = new int[MAXN];
public static void main(String[] args) { Scanner sc = new Scanner(System.in); n = sc.nextInt(); V = sc.nextInt();
for (int i = 1; i <= n; i++) { w[i] = sc.nextInt(); v[i] = sc.nextInt(); }
for (int i = 1; i <= n; i++) { for (int j = w[i]; j <= V; j++) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } }
System.out.println(dp[V]); sc.close(); }}三、多重背包问题
问题描述
有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 s[i] 件,每件体积是 w[i],价值是 v[i]。求解将哪些物品装入背包可使价值总和最大。
朴素解法(三重循环)
for (int i = 1; i <= n; i++) { for (int j = V; j >= w[i]; j--) { for (int k = 1; k <= s[i] && k * w[i] <= j; k++) { dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]); } }}时间复杂度:O(V * Σs[i])
二进制优化
将每种物品的 s[i] 件拆分成若干组,每组的数量为 2 的幂次(1, 2, 4, 8, …),然后转化为 01 背包问题。
拆分原理:任何正整数都可以用若干个 2 的幂次表示。例如 s[i] = 10,可以拆分为 1, 2, 4, 3(1+2+4+3=10)。
二进制拆分图解
代码实现:
import java.util.Scanner;
public class MultipleKnapsack { static final int MAXN = 25005; static int n, V; static int[] w = new int[MAXN]; static int[] v = new int[MAXN]; static int[] dp = new int[MAXN];
public static void main(String[] args) { Scanner sc = new Scanner(System.in); n = sc.nextInt(); V = sc.nextInt();
int cnt = 0; // 拆分后的物品总数 for (int i = 1; i <= n; i++) { int a = sc.nextInt(); // 体积 int b = sc.nextInt(); // 价值 int s = sc.nextInt(); // 数量
int k = 1; while (k <= s) { cnt++; w[cnt] = k * a; v[cnt] = k * b; s -= k; k *= 2; } if (s > 0) { cnt++; w[cnt] = s * a; v[cnt] = s * b; } }
// 01 背包 for (int i = 1; i <= cnt; i++) { for (int j = V; j >= w[i]; j--) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } }
System.out.println(dp[V]); sc.close(); }}例题:AcWing 4. 多重背包问题 I
使用二进制优化解法,时间复杂度降低到 O(V * Σlog(s[i]))。
四、分组背包问题
问题描述
有 N 组物品和一个容量为 V 的背包。第 i 组物品有 s[i] 件,同一组内的物品最多只能选一件。每件物品有体积和价值。求解将哪些物品装入背包可使价值总和最大。
分组背包图解
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i][k]] + v[i][k]) // k 枚举第 i 组的所有物品空间优化(一维数组)
for (int i = 1; i <= n; i++) { // 枚举组 for (int j = V; j >= 0; j--) { // 逆序枚举体积 for (int k = 1; k <= s[i]; k++) { // 枚举组内物品 if (j >= w[i][k]) { dp[j] = Math.max(dp[j], dp[j - w[i][k]] + v[i][k]); } } }}例题:AcWing 9. 分组背包问题
代码实现:
import java.util.Scanner;
public class GroupKnapsack { static final int MAXN = 105; static int n, V; static int[] s = new int[MAXN]; // 每组物品数量 static int[][] w = new int[MAXN][MAXN]; // 每组物品的体积 static int[][] v = new int[MAXN][MAXN]; // 每组物品的价值 static int[] dp = new int[MAXN];
public static void main(String[] args) { Scanner sc = new Scanner(System.in); n = sc.nextInt(); V = sc.nextInt();
for (int i = 1; i <= n; i++) { s[i] = sc.nextInt(); for (int j = 1; j <= s[i]; j++) { w[i][j] = sc.nextInt(); v[i][j] = sc.nextInt(); } }
for (int i = 1; i <= n; i++) { for (int j = V; j >= 0; j--) { for (int k = 1; k <= s[i]; k++) { if (j >= w[i][k]) { dp[j] = Math.max(dp[j], dp[j - w[i][k]] + v[i][k]); } } } }
System.out.println(dp[V]); sc.close(); }}五、混合背包问题
问题描述
有的物品只能用一次(01背包),有的物品可以无限用(完全背包),有的物品有数量限制(多重背包)。
解法
根据物品类型分别处理:
for (int i = 1; i <= n; i++) { if (物品类型 == 01背包) { for (int j = V; j >= w[i]; j--) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } } else if (物品类型 == 完全背包) { for (int j = w[i]; j <= V; j++) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } } else if (物品类型 == 多重背包) { // 使用二进制优化 }}六、二维费用背包
问题描述
物品不仅有重量限制,还有体积限制。第 i 件物品的重量是 w[i],体积是 v[i],价值是 val[i]。背包的最大重量是 W,最大体积是 V。
状态定义
dp[i][j][k] 表示前 i 件物品,重量不超过 j,体积不超过 k 的最大价值。
空间优化(二维数组)
for (int i = 1; i <= n; i++) { for (int j = W; j >= w[i]; j--) { for (int k = V; k >= v[i]; k--) { dp[j][k] = Math.max(dp[j][k], dp[j - w[i]][k - v[i]] + val[i]); } }}例题:AcWing 8. 二维费用的背包问题
代码实现:
import java.util.Scanner;
public class TwoDimensionalKnapsack { static final int MAXN = 105; static int n, W, V; static int[] w = new int[MAXN]; static int[] v = new int[MAXN]; static int[] val = new int[MAXN]; static int[][] dp = new int[MAXN][MAXN];
public static void main(String[] args) { Scanner sc = new Scanner(System.in); n = sc.nextInt(); W = sc.nextInt(); V = sc.nextInt();
for (int i = 1; i <= n; i++) { w[i] = sc.nextInt(); v[i] = sc.nextInt(); val[i] = sc.nextInt(); }
for (int i = 1; i <= n; i++) { for (int j = W; j >= w[i]; j--) { for (int k = V; k >= v[i]; k--) { dp[j][k] = Math.max(dp[j][k], dp[j - w[i]][k - v[i]] + val[i]); } } }
System.out.println(dp[W][V]); sc.close(); }}七、背包问题变种
1. 求方案数
在普通背包的基础上,统计有多少种方案可以达到最大价值。
状态定义:
dp[j]:容量为j的最大价值cnt[j]:达到最大价值的方案数
转移方程:
if (dp[j - w[i]] + v[i] > dp[j]) { dp[j] = dp[j - w[i]] + v[i]; cnt[j] = cnt[j - w[i]];} else if (dp[j - w[i]] + v[i] == dp[j]) { cnt[j] += cnt[j - w[i]];}2. 求具体方案
使用二维数组 dp[i][j] 记录状态,然后倒推出选了哪些物品。
// 倒推方案List<Integer> path = new ArrayList<>();int j = V;for (int i = n; i >= 1; i--) { if (dp[i][j] == dp[i-1][j - w[i]] + v[i]) { path.add(i); j -= w[i]; }}3. 恰好装满背包
初始化时,dp[0] = 0,其他 dp[j] = -∞(表示不可达状态)。
Arrays.fill(dp, Integer.MIN_VALUE / 2);dp[0] = 0;4. 求第 k 优解
维护每个状态的前 k 个最优值。
八、经典例题汇总
| 题目 | 类型 | 链接 |
|---|---|---|
| AcWing 2. 01背包问题 | 01背包 | 链接 |
| AcWing 3. 完全背包问题 | 完全背包 | 链接 |
| AcWing 4. 多重背包问题 I | 多重背包 | 链接 |
| AcWing 5. 多重背包问题 II | 多重背包(二进制优化) | 链接 |
| AcWing 9. 分组背包问题 | 分组背包 | 链接 |
| AcWing 8. 二维费用的背包问题 | 二维背包 | 链接 |
| LeetCode 416. 分割等和子集 | 01背包变种 | 链接 |
| LeetCode 322. 零钱兑换 | 完全背包变种 | 链接 |
| LeetCode 518. 零钱兑换 II | 完全背包(方案数) | 链接 |
九、总结对比
核心差异对比表
| 背包类型 | 特点 | 内层循环方向 | 时间复杂度 |
|---|---|---|---|
| 01 背包 | 每件物品最多选一次 | 逆序(V → 0) | O(NV) |
| 完全背包 | 每件物品无限次 | 正序(0 → V) | O(NV) |
| 多重背包 | 每件物品最多 s 次 | 逆序 + 二进制优化 | O(V·Σlog(s)) |
| 分组背包 | 每组最多选一件 | 逆序 | O(N·V·S) |
| 混合背包 | 混合多种类型 | 根据类型决定 | - |
| 二维费用 | 两个维度限制 | 双重逆序 | O(N·W·V) |
决策流程图
十、刷题建议
- 基础阶段:先刷 AcWing 的背包九讲,掌握基本模板
- 进阶阶段:LeetCode 上的背包变种题,理解状态转移的本质
- 综合应用:Codeforces、AtCoder 等平台的动态规划题
背包问题的核心是理解状态定义和状态转移方程,熟练掌握后可以轻松应对各种变种!
参考资料:
如果觉得本文有帮助,欢迎分享给更多的朋友!
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!