1. 基础的01背包问题
1.0 题目
有 N N N 件物品和一个容量为 V V V 的背包。第 i i i 件物品的体积是 w [ i ] w[i] w[i],价值是 v [ i ] v[i] v[i] ,求将哪些物品装入背包可使价值总和最大。
【输入格式】
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
【输入样例】
4 5
1 2
2 4
3 4
4 5
【输出格式】
输出一个整数,表示最大价值。
【输出样例】
8
1.1 解题方法一(二维)
解题思路1
01背包问题是一个典型的动态规划问题,可以将原问题转化成其子问题进行求解。
- 【base case】:如果物品只有一个:能放得下就放,放不下背包中就不放任何东西。
- 【dp数组】:定义一个二维的数组
dp[i][j]
,表示的含义为 ——对于前 i 件物品,背包容量为 V 时,背包里装的东西的最大价值。
- 【状态转移】:第
i
个物品的体积是否比整个背包的容积还大?- 【如果放不下】:那就不拿第
i
i
i 个物品,和只拿前
i-1
个一样。dp[i][j] = dp[i - 1][j];
- 【如果放得下】:那就再比较一下,
- 取第
i
个,并取剩下容积的最大值; - 不取第
i
个,只拿前i-1
个(和前面的状态相同)。
- 取第
- 【如果放不下】:那就不拿第
i
i
i 个物品,和只拿前
代码1
public class Backpack_1_2 {
public int solution(int num, int maxVolumn, int[][] items) {
if (num == 1 && items[0][1] <= maxVolumn) return items[1][1];
if (num == 1) return 0;
// 【取前 i 个物品,背包容量为 j 】时候的最大价值
int[][] dp = new int[num + 1][maxVolumn + 1];
for (int i = 1; i < num + 1; i++) {
for (int j = 1; j < maxVolumn + 1; j++) {
dp[i][j] = dp[i - 1][j];
if (items[i][0] <= j) { // 如果第 i 个物品装得下
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - items[i][0]] + items[i][1]);
}
}
}
return dp[num][maxVolumn];
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
int maxVolumn = sc.nextInt();
int[][] items = new int[num + 1][2]; // [体积,价值]
for (int i = 1; i < num + 1; i++) {
items[i][0] = sc.nextInt();
items[i][1] = sc.nextInt();
}
Backpack_1_2 obj = new Backpack_1_2();
System.out.println(obj.solution(num, maxVolumn, items));
}
}
打印 dp[][]
数组(为了方便把第0行和第0列空出来了):
//背包容量为:
//0 1 2 3 4 5
0 0 0 0 0 0
0 2 2 2 2 2 // 只考虑第 1 个物品
0 2 4 6 6 6 // 只考虑第 1、2 个物品
0 2 4 6 6 8 // 只考虑第 1、2、3 个物品
0 2 4 6 6 8 // 考虑第 1、2、3、4 个物品
1.2 解题方法二(优化至一维)
解题思路2
从上面的 dp[][]
数组我们可以看出,实际上行与行之间是 递增 的关系(物品多了,组合显然也更多嘛,更容易从中找出更大价值的组合)。
因此,可以进一步将二维的 dp[][]
压缩成一维数组 dp[]
,即 直接把第 i
行的结果覆盖到第 i-1
行,这样只用维护一行数组就行了, 从而优化了空间复杂度。
同理,根据动态规划问题的三步走:
- 【base case】:如果物品只有一个:能放得下就放,放不下背包中就不放任何东西。
- 【dp数组】:定义一维的数组
dp[j]
,含义为 ——总重量不超过 j 的情况下的最大价值。
- 【状态转移】:对于前
i
个物品,考虑当拿了第i
个时,剩下的空间分别要拿什么才能达到最大(依次遍历dp[0]
到dp[剩余空间]
, 其中剩余空间
为总容量 - 第i个物品的体积
)
这里有一个要注意的点:内层的循环,j
必须从 maxVolumn
到 items[i][0]
递减 ,这是因为,要确保 dp[j - items[i][0]]
是 i-1
状态的值,而不是 i
状态下的(同一轮外层for循环中算过的)。
代码2
public class Backpack_1_3 {
public int solution(int num, int maxVolumn, int[][] items) {
if (num == 1 && items[0][1] <= maxVolumn) return items[1][1];
if (num == 1) return 0;
// 【背包容量为 i 】时候的最大价值
int[] dp = new int[maxVolumn + 1];
for (int i = 1; i <= num; i++) { // 对于前 i 个物品
// 这里倒过来是因为:要保证dp[j - items[i][0]] 是i-1状态的值,而不是 i 状态下的
for (int j = maxVolumn; j >= items[i][0]; j--) { // 背包容量为 j
dp[j] = Math.max(dp[j], dp[j - items[i][0]] + items[i][1]);
}
}
return dp[maxVolumn];
}
public static void main(String[] args) {
int num = 4;
int maxVolumn = 5;
int[][] items = {{0, 0}, {1, 2}, {2, 4}, {3, 4}, {4, 5}};
Backpack_1_3 obj = new Backpack_1_3();
System.out.println(obj.solution(num, maxVolumn, items));
}
}
打印 dp[]
数组:
//背包容量为:
//0 1 2 3 4 5
0 2 4 6 6 8
2. 变型的01背包问题
在上面的题目中,题目给出了一个容量为 V V V 的背包,只要所拿的物品不超过这个容积就可以了,但不要求正好装满(不多不少)。
如果现在将上面的基础01背包稍微改动一下,要求:背包必须要装满。 此时该怎么办呢?
解题思路
跟基础01背包代码相比,唯一要做的就是 改动一下dp数组的初始化 即可。除了 dp[0] == 0
,其他的都要初始化为 -inf
。
为什么?
以一维 dp
数组的解法为例,在状态转移的过程中,
dp[j - items[i][0]]
+ items[i][1]
,前者如果
- 正好能被凑齐,那和之前一样;
- 凑不齐,那么它的
dp
值就会是-inf
,在Math.max()
中就不会被选到,从而排除了没有正好塞满背包(凑不齐)的情况。
举例说明:
【输入样例】
物品:
2 4
3 5
4 100
背包容量:
5
初始的dp数组:
[0, -inf, -inf, -inf, -inf, -inf]
第一轮外层for循环(i=1, j = 5 ~ 2):
dp[5] = max(dp[5], dp[3] + 4) = -inf
dp[4] = max(dp[4], dp[2] + 4) = -inf
dp[3] = max(dp[3], dp[1] + 4) = -inf
dp[2] = max(dp[2], 【dp[0] + 4】) = 4
循环结束后的dp数组:
[0, -inf, 2, -inf, -inf, -inf]
第二轮外层for循环(i=2, j = 5 ~ 3):
dp[5] = max(dp[5], 【dp[2] + 5】) = 4+5 = 9
dp[4] = max(dp[4], dp[1] + 5) = -inf
dp[3] = max(dp[3], 【dp[0] + 5】) = 5
循环结束后的dp数组:
[0, -inf, 2, 5, -inf, 9]
第三轮外层for循环(i=3, j = 5 ~ 4):
dp[5] = max(【dp[5]】, dp[1] + 100) = 9
dp[4] = max(dp[4], 【dp[0] + 100】) = 100
循环结束后的dp数组:
[0, -inf, 2, 5, 100, 9]
最后,当背包容量为5时(且要保证正好装满),只能取9。
代码
public int solution(int num, int maxVolumn, int[][] items) {
if (num == 1 && items[0][1] <= maxVolumn) return items[1][1];
if (num == 1) return 0;
// 【背包容量为 i 】时候的最大价值
int[] dp = new int[maxVolumn + 1];
Arrays.fill(dp, Integer.MIN_VALUE);
dp[0] = 0;
for (int i = 1; i <= num; i++) { // 对于前 i 个物品
// 这里倒过来是因为:要保证dp[j - items[i][0]] 是i-1状态的值,而不是算过i情况下的
for (int j = maxVolumn; j >= items[i][0]; j--) { // 背包容量为 j
dp[j] = Math.max(dp[j], dp[j - items[i][0]] + items[i][1]);
}
}
for (var v: dp) {
System.out.print(v + " ");
}
System.out.println();
return dp[maxVolumn];
}