题目
有 N N N件物品和一个容量为 V V V的背包。第i件物品的体积是 c [ i ] c[i] c[i],价值是 w [ i ] w[i] w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大,求出最大价值总和(要区别背包刚好装满和背包可以不装满的两种不同情况下的求解)。
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f
[
i
]
[
v
]
=
m
a
x
(
f
[
i
−
1
]
[
v
]
,
f
[
i
−
1
]
[
v
−
c
[
i
]
]
+
w
[
i
]
)
f[i][v]=max(f[i-1][v],\quad f[i-1][v-c[i]]+w[i])
f[i][v]=max(f[i−1][v],f[i−1][v−c[i]]+w[i])。
这里的 f [ i ] [ v ] f[i][v] f[i][v]定义是选取i个物品其背包容量加起来恰好为v。
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。
- 如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;
- 如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为 v − c [ i ] v-c[i] v−c[i]的背包中”,此时能获得的最大价值就是 f [ i − 1 ] [ v − c [ i ] ] f[i-1][v-c[i]] f[i−1][v−c[i]]再加上通过放入第i件物品获得的价值 w [ i ] w[i] w[i],即 f [ i ] [ v ] = f [ i − 1 ] [ v − c [ i ] ] + w [ i ] f[i][v]=f[i-1][v-c[i]]+w[i] f[i][v]=f[i−1][v−c[i]]+w[i]。
初始值问题
1.要求背包刚好装满时的最大总价值时,此时参照状态转移方程的定义,从初始化的条件改变入手即可。
因为要求为背包恰好装满的情况下,所以初始为0时,即有0个物品被选时,只有 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0有意义,而 f [ 0 ] [ 1 ] f[0][1] f[0][1], f [ 0 ] [ 2 ] f[0][2] f[0][2], f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…均无意义。因为题目要求的是恰好装满,所以在初始为0即没有选任何物品时, f [ 0 ] [ 1 ] f[0][1] f[0][1], f [ 0 ] [ 2 ] f[0][2] f[0][2], f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…这些状态是不应该存在的,因为 f [ 0 ] [ 1 ] f[0][1] f[0][1]的定义为选取0个物品时,其背包容量加起来正好是1,同理, f [ 0 ] [ 2 ] f[0][2] f[0][2]的定义为选取0个物品时,其背包容量加起来正好是2…而这些都是不可能的,因为选择0个物品时,在 f [ i ] [ v ] f[i][v] f[i][v]的定义下,初始值除了 f [ 0 ] [ 0 ] f[0][0] f[0][0]有意义外, f [ 0 ] [ 1 ] f[0][1] f[0][1], f [ 0 ] [ 2 ] f[0][2] f[0][2], f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…这些状态均无意义。
所以在要求背包刚好装满时的最大总价值时,初始设置应为 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0, f [ 0 ] [ 1 ] = f [ 0 ] [ 2 ] = f [ 0 ] [ 3 ] . . . f [ 0 ] [ v ] = − I N F f[0][1]=f[0][2]=f[0][3]...f[0][v]=-INF f[0][1]=f[0][2]=f[0][3]...f[0][v]=−INF。
2.要求背包允许不刚好装满时的最大总价值时,此时参照状态转移方程的定义,也可从初始化的条件改变入手。
因为在得到最大总价值时,背包可以不装满,则初始未选物品即0个物品被选时,除了 f [ 0 ] [ 0 ] f[0][0] f[0][0]有意义, f [ 0 ] [ 1 ] f[0][1] f[0][1], f [ 0 ] [ 2 ] f[0][2] f[0][2], f [ 0 ] [ 3 ] f[0][ 3] f[0][3]…均有意义,因为虽然 f [ 0 ] [ 1 ] f[0][1] f[0][1]的定义为选取0个物品时,其背包容量加起来正好是1,但由于此时背包可以不要求恰好装满等于1即可以少于1,即 f [ 0 ] [ 1 ] f[0][1] f[0][1], f [ 0 ] [ 2 ] f[0][2] f[0][2], f [ 0 ] [ 3 ] f[0][3] f[0][3]… f [ 0 ] [ v ] f[0][v] f[0][v]的存在是有意义的。所以除了设置 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0外,初始化时还需 f [ 0 ] [ 1 ] = f [ 0 ] [ 2 ] = f [ 0 ] [ 3 ] . . . f [ 0 ] [ v ] = 0 f[0][1]=f[0][2]=f[0][3]...f[0][v]=0 f[0][1]=f[0][2]=f[0][3]...f[0][v]=0。
假设有一个体积为10的背包,物品的体积分别为4、5、6,对应的价值为10,17,16 。
由上图可知,
- 若要求在背包v恰好装满时能达到的最大总价值,
则只有 f [ 0 ] [ 0 ] + 10 f[0][0]+10 f[0][0]+10 → \rightarrow → f [ 1 ] [ 4 ] f[1][4] f[1][4] → \rightarrow → f [ 2 ] [ 4 ] + 16 f[2][4]+16 f[2][4]+16 → \rightarrow → f [ 3 ] [ 10 ] f[3][10] f[3][10]这样的一条路,但此时并不是最大总价值,只是说在满足背包恰好刚装满条件下所得的最大总价值。 - 若求背包v可以不恰好装满时能达到的最大总价值,
则可选择 f [ 0 ] [ 1 ] + 10 f[0][1]+10 f[0][1]+10 → \rightarrow → f [ 1 ] [ 5 ] + 17 f[1][5]+17 f[1][5]+17 → \rightarrow → f [ 2 ] [ 10 ] f[2][10] f[2][10] → \rightarrow → f [ 3 , 10 ] f[3,10] f[3,10]这样的一条路,此时是最大总价值。
结果枚举问题
由上图分析可知,无论在要求恰好装满或者不要求恰好装满的条件下,最终得到的结果均不需要枚举,即cout<< f [ N ] [ V ] f[N][V] f[N][V]<<endl; 即可。
#include<iostream>
using namespace std;
#include<algorithm>
// 注意,若在函数内部则初始化是随机的,在全局上定义时默认初始化为 0
// 所以这里无需 c[105] = {0}...
int c[105], w[105];
int dp[105][1005];
int main(){
int v, n;
cin >> v >> n;
for (int i = 1; i <= n; i++)
cin >> c[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = v; j >= 0; j--) {// 二维数组时,j使用顺序或逆序均可以
if(j >= c[i])
dp[i][j] = max(dp[i-1][j - c[i]] + w[i], dp[i-1][j]);
else
dp[i][j] = dp[i-1][j];
}
cout << dp[n][v] << endl;
return 0;
}
/*
注意:
当设置为二维数组时,这里的第三个for循环不可写成
for(int i = 1; i <= n; i++)
for(int j = v; j >= c[i]; j--)
dp[i][j] = max(dp[i-1][j-c[i]] + w[i], dp[i-1][j]);
因为若改成如上形式,
则当j<c[i]时,dp[i][j]的内容得不到上一次dp[i-1][j]的更新;
但是在改为一维数组时,可以改成
for(int i = 1; i <= n; i++)
for(int j = v; j >= c[i]; j--)
dp[j] = max(dp[j-c[i]] + w[i], dp[j]);
因为一维数组时,
当则当j<c[i]时,dp[j]的内容默认为上一次状态的数组dp[j]保持不变。
*/
优化空间复杂度(使用一维数组)
以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。
f [ v ] = m a x ( f [ v ] , f [ v − c [ i ] ] + w [ i ] ) f[v]=max(f[v],\quad f[v-c[i]]+w[i]) f[v]=max(f[v],f[v−c[i]]+w[i])恰就相当于我们的转移方程 f [ i ] [ v ] = m a x ( f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − c [ i ] ] + w [ i ] ) f[i][v]=max(f[i-1][v],\quad f[i- 1][v-c[i]]+w[i]) f[i][v]=max(f[i−1][v],f[i−1][v−c[i]]+w[i]),
因为现在的 f [ v − c [ i ] ] f[v-c[i]] f[v−c[i]]就相当于原来的 f [ i − 1 ] [ v − c [ i ] ] f[i-1][v-c[i]] f[i−1][v−c[i]]。
-
而一维数组时,j如果使用顺序即从小到大求,则因为后面的 f [ k ] f[k] f[k]使用到了前面的 f [ k − c [ i ] ] f[k-c[i]] f[k−c[i]]可能已经被更新了,则相当于 f [ i ] [ k ] f[i][k] f[i][k]由 f [ i ] [ k − c [ i ] ] f[i][k-c[i]] f[i][k−c[i]]更新,显然不对。
”由于v是从小到大,在求等式左边的f[v]前,某个 f [ k ] ( k < v ) f[k](k<v) f[k](k<v)就改了值了,而在求 f [ v ] f[v] f[v]时,这个 v − c [ i ] v-c[i] v−c[i]可能又等于k,而这个k值之前被改过,即 f [ k ] f[k] f[k]已不是 i − 1 i-1 i−1时刻的值,而是i时刻的值,但我们现在需要的是i-1时刻的值来求出i时刻的值,所以通过 f [ v − c [ i ] ] f[v-c[i]] f[v−c[i]]求出的 f [ v ] f[v] f[v]值就是错误的值,与本题意不符合。“ -
而逆序时,j是从v大到小, f [ v − c [ i ] ] f[v-c[i]] f[v−c[i]]中的 v − c [ i ] v-c[i] v−c[i]在此时肯定没变,因为是从后面往前面更新的,即 f [ v − c [ i ] ] f[v-c[i]] f[v−c[i]]的更新也是排在 f [ v ] f[v] f[v]更新之后才进行更新的。
#include<iostream>
using namespace std;
#include<algorithm>
// 注意,若在函数内部则初始化是随机的,在全局上定义时默认初始化为 0
// 所以这里无需 c[105] = {0}...
int c[105], w[105];
int dp[1005];
int main(){
int v, n;
cin >> v >> n;
for (int i = 1; i <= n; i++)
cin >> c[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = v; j >= c[i]; j--)
dp[j] = max(dp[j - c[i]] + w[i], dp[j]);
cout << dp[v] << endl;
return 0;
}
/*
注意:
一维数组时,可以改成
for(int i = 1; i <= n; i++)
for(int j = v; j >= c[i]; j--)
dp[j] = max(dp[j-c[i]] + w[i], dp[j]);
因为一维数组时,
当则当j<c[i]时,dp[j]的内容默认为上一次状态的数组dp[j]不需要赋值改变。
*/
链接: