一、0-1背包
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值,每件物品只能被添加一次。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
- 第 i 件物品添加到背包中,前i-1件物品总体积不超过j-w的最大价值再加上第i件物品的价值,dp[i][j] = dp[i-1][j-w] + v。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
// W 为背包总体积
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; j++) {
if (j >= w) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时:
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
public int knapsack(int W, int N, int[] weights, int[] values) {
int[] dp = new int[W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
//逆序避免前一行的值被覆盖
for (int j = W; j >= 1; j--) {
if (j >= w) {
dp[j] = Math.max(dp[j], dp[j - w] + v);
}
}
}
return dp[W];
}
二、完全背包
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品组合体积不超过 j 的情况下能达到的最大价值,每件物品都可以重复使用。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否被使用,可以分两种情况讨论:
- 第 i 件物品未被使用,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的只使用前 i-1 件物品组合的最大价值,dp[i][j] = dp[i-1][j]。
- 第 i 件物品被使用,使用前i件物品组合总体积不超过j-w的最大价值再加上第i件物品的价值,dp[i][j] = dp[i][j-w] + v,此时dp[i][j]至少使用了一次第i件物品。
第 i 件物品可使用也可以不使用来进行组合,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
// W 为背包总体积
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; j++) {
if (j >= w) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - w] + v);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
空间优化
在程序实现时可以对完全背包做优化。观察状态转移方程可以知道,其实每一行单元的值的填写只要看它的左边的值。如果没有左边,它至少是上一行单元格的值。所以该状态方程可以转换为:
第一行表示该单元没有左边,所以取上一行单元格的值;第二行表示该单元值的填写只要看它的左边的值。如果不理解,建议将二维dp表格全部亲手填一遍。
因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时:
public int knapsack(int W, int N, int[] weights, int[] values) {
int[] dp = new int[W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
//必须从w开始
for (int j = w; j <= W; j++) {
dp[j] = Math.max(dp[j], dp[j - w] + v);
}
}
return dp[W];
}
三、对比
问题 | 状态方程 | 优化空间后的状态方程 | 区别 |
0-1背包 | 「0 - 1」背包参考上一行,所以「0 - 1」背包 倒序填表 | ||
完全背包 | 「完全背包」参考当前行,所以完全背包正序填表 |