有一个容量为 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 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v
。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)
// 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];
}
空间优化:
观察方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)
- 上述方法是用的二维dp数组存储的子问题解。假设i为数组的行数,j为数组的列数。那么由方程易知第i行的子问题解只与第i-1行子问题解有关系。或者说当前行的
dp[i][x]
0 <= x <= W
,由前一行的子问题解推出dp[i-1][y]
0 <= y <= W
。 - 那么如果能够用当前行的dp数据覆盖前一行的dp数据,就可以只用一维数组存储子问题的解。
- 若可以只用一维数组存储,外层循环i还是从1到N,但是方程变为:
dp[j] = max(dp[j], dp[j-w] + v)
。 - 下面分析当i==a时 (1 <= a <= N),如果j从小到大遍历,会导致计算dp[j]出错,因为dp[j]是由i ==a-1时算出来的dp[j]和dp[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];
}
再简化一下代码
public int knapsack(int W, int N, int[] weights, int[] values) {
int[] dp = new int[W + 1];
for (int i = 1; i <= N; ++i)
for (int j = weights[i-1]; j >= 1; --j)
if (dp[j] < dp[j-weights[i-1]] + values[i-1])
dp[j] = dp[j-weights[i-1]] + values[i-1];
return dp[W];
}