背包 Knapsack
0-1背包问题
有 N 种物品,一个容量为 V 的背包,第 i 件物品的体积为 cap[i],价值为 val[i],求在不超过背包容量限制的情况下所能获得的最大物品价值和为多少?
因为在这个问题中,一件物品要么不选,要么选,正好对应于 0-1 两个状态,所以我们一般把形如这样的背包问题称作 0-1 背包问题。
状态:
定义状态:
knapsack[i][j]代表在前 i 件物品中选择若干件,这若干件物品的体积和 不超过j 的情况下,所能获得的最大物品价值和。
for (int i = 1; i <= N; ++i) {
for (int j = 0; j <= V; ++j) {
if (j >= cap[i-1]) {
dp[i][j] = max(dp[i-1][j], //没用第i个
dp[i - 1][j - cap[i-1]] + val[i-1]); //用第i个
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
//ans at dp[N][V]
Time: O(NV)
Space: O(NV)
优化:
滚动数组:
观察到在计算 backpack[i][j] 时,我们只用到了 backpack[i−1][j] 和 backpack[i−1][j−cap[i]]这两个数,并没有用到 backpack[i−1][],backpack[i−2][],… 的信息,也就是 backpack[i][]只和 backpack[i−1][] 有关,那么我们只需要两个数组就够了。
for(int i =1; i <=N;i++){
for(int j = 1; j<=V;j++){
if(j>=A[i-1]){
f[i%2][j] = max(f[(i-1)%2][j],f[(i-1)%2][j-A[i-1]]+V[i-1]);
}else{
f[i%2][j] = f[(i-1)%2][j];
}
}
}
return f[n%2][m];
一维数组:
把第二层循环倒序即可
在第 i 层循环初 f[j] 存的相当于 backpack[i−1][j] 的值。
在更新 f[j] 时,我们用到了 f[j−cap[i]],由于第二层循环倒序,所以 f[j−cap[i]] 未被更新,此时它代表 backpack[i−1][j−cap[i]]。所以 f[j] = Math.max(f[j], f[j - cap[i]] + val[i])
等价于 backpack[i][j] = Math.max(backpack[i - 1][j], backpack[i - 1][j - cap[i]] + val[i])
。
在第 i 层循环末 f[j] 存的相当于 backpack[i][j] 的值。
for (int i = 0; i < N; i++) {
for (int j = V; j >= cap[i]; j--)
dp[j] = max(dp[j], dp[j - cap[i]] + val[i]);
//ans at dp[N]
对于二维数组的背包来说,正序和逆序是无所谓的,因为你把状态都保存了下来,而一维数组的背包是会覆盖之前的状态的。
多重背包问题 (Bounded knapsack)
有 N 种物品,一个容量为 V 的背包,第 i 种物品的数量为 num[i],体积为 cap[i],价值为 val[i],求在不超过背包的容量限制的情况下所能获得的最大物品价值和为多少?
这个问题与 0-1 背包的区别在于,0-1 背包中每种物品有且只有一个,而多重背包中一种物品有 nums[i]个。我们一般把形如这样的背包问题称作多重背包问题。
问题分析
求解这个问题一个简单的思路就是可以把它看成一个拥有 ∑num[i] 件物品的 0-1 背包问题。
另一种简单的思路是将它看成一个拥有 N 件物品的 0-1 背包问题,但是第 i 件物品的体积和价值有 num[i] 种取值。
Time: O(V∑num[i])
Space: O(NV)
for (int i = 1; i <= N; ++i) {
for (int j = 0; j <= V; ++j) {
for(int k = 1; k <= num[i-1]; k++){
if (j >= k*cap[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i - 1][j - k*cap[i-1]] + k*val[i-1]);
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
}
//ans at dp[N][V]
优化的方法十分简单,我们只需要将 j
的循环逆向(如果正向,不知道i类物品有没有超过他的数量限制)
for (int i = 0; i < N; ++i) {
for (int k = 1; k <= num[i]; ++k) {
for (int j = V; j >= cap[i]; --j) {
f[j] = max(f[j], f[j - cap[i]] + val[i]);
}
}
}
完全背包问题 (unbounded knapsack)
有 N 种物品,一个容量为 V 的背包,每种物品都有无限件可用。第 i 种物品的体积为 cap[i],价值为 val[i]。求在不超过背包的容量限制的情况下所能获得的最大总价值和为多少?
虽然题目声称每种物品都有无限件,但实际上每种物品最多有 V / cap[i] 件,因此这个问题相当于一个 num[i] = V / cap[i]的多重背包问题。
for (int i = 0; i < N; ++i) {
for (int k = 1; k * cap[i] <= V; ++k) {
for (int j = V; j >= cap[i]; --j) {
f[j] = max(f[j], f[j - cap[i]] + val[i]);
}
}
}
时间复杂度优化
目前我们得到了一个时间复杂度为 O(V∑A[i]) 的算法来解决完全背包问题。
而且优化的方法十分简单,我们只需要将 j
的循环由逆向转变为正向即可。(i类物品可以无限次放入背包)
由于同一种物品的个数无限,所以我们可以在任意容量 j 的背包尝试装入当前物品,j 从小向大枚举可以保证所有包含第 i 种物品,体积不超过 j−A[i] 的状态被枚举到。
int backPack(vector<int> &cap, vector<int> &val, int m) {
int n = A.size();
vector<int> dp(m+1,0);
for(int i = 0; i < n; i++)
for(int j = cap[i]; j <=m; j++)
dp[j] = max(dp[j],dp[j-cap[i]]+val[i]);
return dp[m];
}
从而得到时间复杂度为 O(NV),空间复杂度为 O(V)的优秀算法。
首先想想为什么01背包中要按照j=V...0的逆序来循环。这是因为要保证第ii次循环中的状态f[i][j]是由状态f[i−1][j−w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i−1][j−w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第ii种物品的子结果f[i][j−w[i]],所以就可以并且必须采用j=0...V的顺序循环。
总结:
问题的简化:复杂问题转化为0-1问题
问题的优化:滚动数组,一维数组。一维数组注意j(体积)的循环逆向(0-1,多重)和正向(完全)。逆向是防止一件物品多次放入背包。