背包问题总结分析
背包问题是个很经典的动态规划问题,本博客对背包问题及其常见变种的解法和思路进行总结分析
01背包
问题介绍
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 v[i],价值是 w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
基本思路
定义int[][] dp,dp[i][j] 表示当容量为j时,对于前i个物品而言的最优放置策略(即最大价值)。对于物品 i 而言,只有放与不放,这两种选择。因此可以得到 状态转移方程:
放物品 i :dp[i][j] = dp[i - 1][j - v[i]] + w[i];
不放物品 i :dp[i][j] = dp[i - 1][j]。
直观方法:
// v和w数组长度都是 N + 1,v[0]和w[0]都是0
private static void backpack1(int N, int V, int[] v, int[] w) {
int[][] dp = new int[N + 1][V + 1];
for (int i = 1; i <= N; ++i) {
for (int j = 1; j <= V; ++j) {
dp[i][j] = dp[i - 1][j];
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]);
}
这种方法空间不是最优的。观察代码发现,dp[i]只跟dp[i-1]有关,所以可以将二维降成一维。
优化方法:
private static void backpack2(int N, int V, int[] v, int[] w) {
int[] dp = new int[V + 1];
for (int i = 1; i <= N; ++i) {
for (int j = V; j >= v[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
System.out.println(dp[V]);
}
注意:
内层循环不能顺序枚举。dp[j - v[i]]实际上相当于 dp[i - 1][j - v[i]],而不是dp[i][j - v[i]],如果顺序枚举, dp[i] 的 j – v[i] 的位置已经被计算过,覆盖了。所以应该通过倒序枚举来规避这个问题。
两个要点:
若 dp[] 全部初始化为0,计算结果的 dp[V] 就是答案;
若 dp[0] 初始化为0,其它元素全部初始化为负无穷,则最后遍历dp[]得到最大值为答案。
解释如下:
dp[V] 一定是最大值。同样遍历了所有物品情况下,容量 V 大于 V – X ,最后得到的价值 dp[V] 必然大于 dp[V – X]。
dp数组初始化值全为 0 ,则允许dp[V]从任何一个初始项转化而来,并不一定是 dp[0]。最终结果如果从 dp[k] 转化而来,说明有 k 体积的空余。但是,如果我们更改一下dp数组初始化的情况: