目录
[1. 01背包](#1. 01背包)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
1. 01背包
描述
你有一个背包,最大容量为 𝑉V。现有 𝑛n 件物品,第 𝑖i 件物品的体积为 𝑣𝑖vi,价值为 𝑤𝑖wi。研究人员提出以下两种装填方案:
1. 1. 不要求装满背包,求能获得的最大总价值;
2. 2. 要求最终恰好装满背包,求能获得的最大总价值。若不存在使背包恰好装满的装法,则答案记为 00。
输入描述:
第一行输入两个整数 𝑛n 和 𝑉(1≦𝑛,𝑉≦103)V(1≦n,V≦103),分别表示物品数量与背包容量。
此后 𝑛n 行,第 𝑖i 行输入两个整数 𝑣𝑖,𝑤𝑖(1≦𝑣𝑖,𝑤𝑖≦103)vi,wi(1≦vi,wi≦103),分别表示第 𝑖i 件物品的体积与价值。
输出描述:
输出两行:
1. 1. 第一行输出方案 11 的答案;
2. 2. 第二行输出方案 22 的答案(若无解输出 00)。
示例1
输入:
3 5
2 10
4 5
1 4
复制输出:
14
9
复制说明:
在该组样例中:
∙ ∙ 选择第 11、第 33 件物品即可获得最大价值 10+4=1410+4=14(未装满);
∙ ∙ 选择第 22、第 33 件物品可使背包体积 4+1=54+1=5 恰好装满且价值最大,为 5+4=95+4=9。
示例2
输入:
3 8
12 6
11 8
6 8
复制输出:
8
0
复制说明:
装第三个物品时总价值最大但是不满,装满背包无解。
1.1 题目解析
题目本质
经典的 01 背包问题,但有两个变种------一个允许背包有剩余空间,另一个要求恰好装满。核心是"在容量限制下,如何选择物品使价值最大",本质上是线性DP。
常规解法
暴力枚举所有物品的选/不选组合(2^n 种),计算每种组合的总体积和总价值,筛选出符合条件的最大值。
java
// 常规解法:暴力枚举(会超时)
public class Solution {
static int maxValue1 = 0; // 方案1:不要求装满
static int maxValue2 = 0; // 方案2:恰好装满
public static void dfs(int[] v, int[] w, int index, int curV, int curW, int V) {
if (index == v.length) {
// 方案1:只要不超容量
maxValue1 = Math.max(maxValue1, curW);
// 方案2:必须恰好装满
if (curV == V) {
maxValue2 = Math.max(maxValue2, curW);
}
return;
}
// 不选当前物品
dfs(v, w, index + 1, curV, curW, V);
// 选当前物品
if (curV + v[index] <= V) {
dfs(v, w, index + 1, curV + v[index], curW + w[index], V);
}
}
}
问题分析
暴力枚举的时间复杂度是 O(2^n),当 n=1000 时完全不可行。问题在于存在大量重复计算------比如"前 5 个物品选了 1、3、5"和"前 5 个物品选了 3、1、5"本质是同一个状态,但会被重复计算。
思路转折
要想高效 → 必须消除重复计算 → 动态规划。关键观察:当前状态只依赖"处理了多少个物品"和"当前背包容量",与选择的顺序无关。定义 dp[i][j] 表示"前 i 个物品,容量为 j 时的最优解",通过递推逐步构建答案,时间复杂度降至 O(nV)。
1.2 解法
算法思想
两个方案的核心区别在于 DP 初始化和状态有效性:
-
外层循环
i:遍历第 i 个物品(处理第 i 个物品)。 -
内层循环
j:遍历当前背包的容量 j(从 1 到最大容量 v)。
- 方案1(不装满):dp[i][j] = 前 i 个物品,容量 ≤ j 时的最大价值
-
递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])
- 初始状态:dp[0][j] = 0(所有容量都合法,最差不选任何物品)
- 方案2(恰好装满):dp[i][j] = 前 i 个物品,容量 = j 时的最大价值
-
递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])(需判断状态有效性)
- 初始状态:dp[0][0] = 0,dp[0][j] = -1(j>0 时无法装满,标记为无效)
步骤拆解
方案1:不要求装满
**i)**初始化:dp[0][j] = 0(所有容量默认为 0,表示不选物品)
**ii)**遍历物品 i(1 到 n)和容量 j(1 到 V)
**iii)**状态转移:
-
不选物品 i:dp[i][j] = dp[i-1][j]
-
选物品 i(若容量够):dp[i][j] = dp[i-1][j-v[i]] + w[i]
-
取两者最大值
**iv)**答案:dp[n][V]
方案2:恰好装满
**i)**初始化:dp[0][0] = 0,dp[0][j] = -1(j>0,标记"无法装满")
**ii)**遍历物品 i(1 到 n)和容量 j(1 到 V)
**iii)**状态转移:
-
不选物品 i:dp[i][j] = dp[i-1][j]
-
选物品 i(若容量够 且上一状态有效 )
dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]] + w[i]);
**iv)**答案案:dp[n][V] == -1 ? 0 : dp[n][V]
易错点
- 方案2 的初始化:必须将 dp[0][j](j>0)初始化为 -1,而不是 0。0 表示"合法状态,价值为 0",-1 表示"不可达状态"。
- 方案2 的状态转移判断:必须检查 dp[i-1][j-v[i]] != -1,否则会从"无法装满"的无效状态转移,导致错误结果。
-
二维数组重用:两个方案共用 dp 数组时,方案2 开始前必须逐行清零(Arrays.fill(dp[i], 0)),而不是 Arrays.fill(dp, 0)(后者只清空引用)。
-
循环边界:容量循环可以从 0 或 1 开始,但从 1 开始更清晰(因为 dp[i][0] 始终为 0)。物品循环必须从 1 到 n(包含 n)。
1.3 代码实现
java
import java.util.Arrays;
import java.util.Scanner;
public class Main {
private static final int N = 1010;
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];
int[] w = new int[N];
int[][] dp = new int[N][N];
for (int i = 1; i <= n; i++) {
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
// 方案1:不要求装满
// dp[i][j] 表示前 i 个物品,容量不超过 j 的最大价值
// 初始状态:dp[0][j] = 0(默认值,表示不选任何物品)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= V; j++) {
dp[i][j] = dp[i - 1][j]; // 不选物品 i
if (j >= v[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
}
System.out.println(dp[n][V]);
// 方案2:恰好装满
// dp[i][j] 表示前 i 个物品,容量恰好为 j 的最大价值
// 初始状态:dp[0][0] = 0,dp[0][j] = -1(j>0 时无法装满)
for (int i = 0; i <= n; i++) {
Arrays.fill(dp[i], 0);
}
for (int j = 1; j <= V; j++) {
dp[0][j] = -1; // 标记为无效状态
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= V; j++) {
dp[i][j] = dp[i - 1][j]; // 不选物品 i
// 选物品 i:必须判断上一状态是否有效
if (j >= v[i] && dp[i - 1][j - v[i]] != -1) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
}
System.out.println(dp[n][V] == -1 ? 0 : dp[n][V]);
scanner.close();
}
}
复杂度分析
- 时间复杂度:O(nV),两层循环遍历所有状态,每个状态 O(1) 转移
- 空间复杂度:O(nV),使用二维 dp 数组存储所有状态