01背包问题简介
01背包问题是一种经典的动态规划问题。问题描述为:给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,如何选择物品装入背包,使得背包中的物品总价值最大。每种物品只能选择装入或不装入背包一次(即0或1次),这就是“01”的含义。
问题参数
- 物品数量 (
n
):背包中可供选择的物品总数。 - 物品重量 (
weights[i]
):第i
个物品的重量。 - 物品价值 (
values[i]
):第i
个物品的价值。 - 背包容量 (
W
):背包能够承载的最大重量。
目标
- 最大化背包中物品的总价值,同时确保所有物品的总重量不超过背包的容量。
动态规划解法
对于这个问题,我们可以使用动态规划(DP)的方法来解决。我们可以定义一个二维数组dp[i][j]
,其中dp[i][j]
表示在前i
个物品中,当背包容量为j
时,能够装入背包的最大价值。状态转移方程为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
其中,w[i]
是第i
个物品的重量,v[i]
是第i
个物品的价值。如果第i
个物品的重量大于背包的剩余容量j
,那么该物品不能装入背包,此时dp[i][j]
就等于dp[i-1][j]
(即不装入第i
个物品时的最大价值)。否则,我们可以选择装入第i
个物品或者不装入,取这两种情况下的较大值。
int knapsack01(int W, vector<int>& weights, vector<int>& values, int n) {
vector<vector<int>> dp(n + 1, vector<int>(V + 1, 0));
//dp[i][j]:前i个的物品,所装的物品体积不超过j时,能够装下的最大价值
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j];
if (j - vw[i - 1][0] >= 0) {
dp[i][j] = max(dp[i - 1][j - vw[i - 1][0]] + vw[i - 1][1], dp[i][j]);
}
}
}
return dp[n][V];
}
然而,在实际应用中,我们可以将二维数组优化为一维数组,以节省空间。
我们定义一个数组dp
,其中dp[j]
表示在容量为j
的背包中能够装入的最大价值。
状态转移方程
对于每个物品i
和每个容量j
,我们有以下选择:
- 不装入第
i
个物品:此时背包的价值就是前i-1
个物品在容量为j
的背包中的最大价值,即dp[j] = dp[j]
(实际上这一步在循环中通常不会显式执行,因为dp[j]
在更新前已经保存了前一个状态的值)。 - 装入第
i
个物品:如果第i
个物品的重量小于或等于当前考虑的容量j
,我们可以选择将其装入背包,此时背包的价值就是前i-1
个物品在容量为j-weights[i]
的背包中的最大价值加上第i
个物品的价值,即dp[j] = dp[j-weights[i]] + values[i]
。
我们需要在两种选择中取较大值来更新dp[j]
。
初始化
dp[0]
应该初始化为0,表示容量为0的背包无法装入任何物品,因此价值为0。- 其余
dp[j]
(j > 0
)可以初始化为一个非常小的数(如负无穷),但在实际编程中,由于我们总是从较小的容量向较大的容量更新dp
数组,且每次更新都是取较大值,因此通常不需要显式初始化(除了dp[0]
)。
遍历顺序
- 物品遍历:通常从第一个物品遍历到最后一个物品。
- 容量遍历:对于每个物品,我们需要从背包的最大容量
W
向下遍历到该物品的重量(包括)。这是为了确保在计算dp[j]
时,dp[j-weights[i]]
已经包含了不考虑当前物品时的最优解。
int knapsack01(int W, vector<int>& weights, vector<int>& values, int n) {
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; ++i) {
for (int j = W; j >= weights[i]; --j) {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[W];
}
关于01背包问题,还有另一个问题:若背包恰好装满,求至多能装多大价值的物品?
当背包需要恰好装满,并且我们要求在这个条件下至多能装多大价值的物品时,我们可以使用与01背包问题相同的动态规划解法,但在输出结果时需要注意一个关键点:我们需要检查在背包容量W
时是否达到了一个非零的价值,因为这表明我们可以恰好装满背包。
在01背包问题的标准解法中,我们使用一个一维数组dp
,其中dp[j]
表示容量为j
的背包所能装载的最大价值。对于每个物品,我们遍历所有可能的背包容量(从大到小),并更新dp
数组以考虑是否将该物品加入背包。
当背包需要恰好装满时,我们需要特别关注dp[W]
的值。如果dp[W]
是非零的,那么这意味着存在一个选择方案,可以使得背包恰好装满,并且其价值等于dp[W]
。如果dp[W]
仍然是初始值(在大多数情况下是0,但也可能取决于你的初始化方式),那么这表明不存在一个方案可以恰好装满背包。
实现步骤
-
初始化:将
dp[0]
设置为0(空背包的价值为0),其他dp[j]
(对于j > 0
)可以设置为一个非常小的数(尽管在01背包问题中,由于我们总是取最大值,所以它们通常不会被访问到,除非你试图在背包未装满时得到部分解)。然而,在这个特定问题中,你只需要关心dp[W]
,所以除了dp[0]
之外的其他元素的具体值并不重要。 -
动态规划过程:对于每个物品,从背包容量
W
开始向下遍历到该物品的重量。对于每个容量j
,检查是否应该将该物品加入背包(即,如果j >= weights[i]
,则比较dp[j]
和dp[j-weights[i]] + values[i]
)。 -
结果检查:在动态规划过程完成后,检查
dp[W]
的值。如果dp[W]
是非零的,那么它就是在背包恰好装满的情况下可以装载的最大价值。如果dp[W]
是零,那么不存在一个方案可以恰好装满背包。
int knapsack01ExactFull(int W, vector<int>& weights, vector<int>& values, int n) {
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; ++i) {
for (int j = W; j >= weights[i]; --j) {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
}
}
// 如果dp[W]非零,表示可以恰好装满背包
if (dp[W] > 0) {
return dp[W];
} else {
// 如果需要,可以在这里处理无法恰好装满背包的情况
return -1; // 或者其他适当的错误代码/值
}
}