动态规划中的背包问题(Knapsack Problem)是经典问题之一,通常用来解决选择一组物品放入背包使得背包的价值最大化的问题。根据问题条件的不同,背包问题有很多种变体,如0-1背包问题、完全背包问题、多重背包问题等。这里,我们详细介绍最经典的0-1背包问题,并提供代码的详细解读。
1. 0-1背包问题简介
在0-1背包问题中,有一个容量为 C
的背包和 n
件物品。每件物品有两个属性:重量 w[i]
和 价值 v[i]
。目标是选择若干件物品放入背包,使得总重量不超过 C
,并且背包中物品的总价值最大化。
问题的约束:
- 每件物品要么选择(放入背包),要么不选择,因此称为 0-1 背包问题。
- 背包的总重量不能超过
C
。 - 要最大化背包中物品的总价值。
2. 动态规划的思路
动态规划适用于背包问题,因为它具有最优子结构和重叠子问题的性质。解决这个问题的核心在于:针对每一个物品,都有两种选择——要么放进背包,要么不放进背包。
状态定义
定义 dp[i][j]
表示前 i
件物品放入容量为 j
的背包时所能获得的最大总价值。
状态转移方程
对于每件物品 i
:
- 如果不将第
i
件物品放入背包,则最大价值就是dp[i-1][j]
,即前i-1
件物品的最大价值。 - 如果将第
i
件物品放入背包,则最大价值为dp[i-1][j-w[i]] + v[i]
,即前i-1
件物品在剩余容量j-w[i]
时的最大价值加上当前物品的价值v[i]
。
综合起来,状态转移方程为:
dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i])
初始条件
当背包容量为0时,无论选哪件物品,最大价值都是0,即 dp[i][0] = 0
。
3. Java代码实现
public class Knapsack {
public static void main(String[] args) {
// 定义物品的重量和价值
int[] weights = {2, 3, 4, 5}; // 每个物品的重量
int[] values = {3, 4, 5, 6}; // 每个物品的价值
int capacity = 8; // 背包容量
int n = weights.length; // 物品数量
// 计算并输出背包的最大价值
System.out.println("Maximum value in Knapsack = " + knapsack(weights, values, n, capacity));
}
// 动态规划方法解决0-1背包问题
public static int knapsack(int[] weights, int[] values, int n, int capacity) {
// 定义DP数组:dp[i][j] 表示前 i 件物品在容量为 j 时的最大价值
int[][] dp = new int[n + 1][capacity + 1];
// 填充DP表
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= capacity; j++) {
if (weights[i - 1] <= j) { // 当前物品可以放入背包
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
} else { // 当前物品无法放入背包
dp[i][j] = dp[i - 1][j];
}
}
}
// 返回最后一个格子的值,即最大价值
return dp[n][capacity];
}
}
4. 详细解释代码
4.1 输入和输出
在 main
方法中,定义了物品的重量数组 weights[]
、价值数组 values[]
,以及背包的总容量 capacity
和物品数量 n
。然后调用 knapsack()
方法来计算背包中可以获得的最大价值。
4.2 knapsack()
函数详解
public static int knapsack(int[] weights, int[] values, int n, int capacity) {
// 定义DP数组:dp[i][j] 表示前 i 件物品在容量为 j 时的最大价值
int[][] dp = new int[n + 1][capacity + 1];
首先,定义了二维数组 dp[][]
,其中 dp[i][j]
表示前 i
件物品在背包容量为 j
时能够获得的最大总价值。数组的大小为 [n+1][capacity+1]
,因为我们需要处理物品数量从0到n、容量从0到capacity的所有情况。
4.3 初始化和状态转移
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= capacity; j++) {
if (weights[i - 1] <= j) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
接下来,用两个嵌套的 for
循环遍历物品和背包容量,进行状态转移:
- 外层循环
i
遍历每一件物品。 - 内层循环
j
遍历背包的每个可能容量。
对于每个物品 i
:
- 如果该物品的重量
weights[i-1]
小于或等于当前容量j
,可以选择放入背包或不放入背包,选择价值最大的方案。这就是通过状态转移方程dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
计算出的。 - 如果该物品的重量
weights[i-1]
超过当前容量j
,则无法将其放入背包,此时只能继承前i-1
件物品的最大价值,即dp[i][j] = dp[i-1][j]
。
4.4 返回最大价值
return dp[n][capacity];
循环结束后,dp[n][capacity]
就是前 n
件物品在背包容量为 capacity
时能够获得的最大价值,因此返回该值。
5. 复杂度分析
- 时间复杂度:
O(n * capacity)
,因为我们需要填充一个大小为n * capacity
的表。 - 空间复杂度:
O(n * capacity)
,因为我们使用了二维数组dp[][]
存储中间结果。
6. 空间优化
在上述实现中,我们用了二维数组 dp[][]
来存储所有状态。但实际上,在每一行 i
的状态转移时,只依赖于上一行 i-1
的值。因此,我们可以将二维数组压缩为一维数组,从而降低空间复杂度到 O(capacity)
。
public static int knapsackOptimized(int[] weights, int[] values, int n, int capacity) {
int[] dp = new int[capacity + 1];
for (int i = 1; i <= n; i++) {
// 从大到小遍历容量,保证每个物品只被计算一次
for (int j = capacity; j >= weights[i - 1]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i - 1]] + values[i - 1]);
}
}
return dp[capacity];
}
关键点:
- 在每次迭代中,从容量
capacity
开始递减遍历,确保每个物品只更新一次。这种写法防止了在同一轮次中更新状态值时物品被重复选择。
7. 总结
0-1背包问题通过动态规划求解,有明确的状态转移方程。使用二维DP表来记录每个物品在不同容量下的最大价值,最终得到最优解。通过压缩空间,可以进一步优化到一维DP表。