问题描述
给定 n 种物品和一个容量为 c的背包,物品 i 的重量是 wi,其价值为 vi 。
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
思路
- 面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次
- 声明一个 大小为 dp[n][c] 的二维数组,dp[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值
- j < w[i] 的情况,这时候背包容量不足以放下第 i 件物品,只能选择不拿
dp[ i ][ j ] = dp[ i-1 ][ j ](因为不放第i个物品,所以当物品为i时的背包容量=当物品为i-1时的背包容量) - j>=w[i] 的情况,这时背包容量可以放下第 i 件物品,我们就要考虑拿这件物品是否能获取更大的价值,即我们要不要拿这个物品。
1)如果拿,dp[ i ][ j ]=dp[ i-1 ][ j-w[ i ] ] + v[ i ]。 这里的dp[ i-1 ][ j-w[ i ] ]指的就是考虑了i-1件物品,背包容量为j-w[i]时的最大价值,也是相当于为第i件物品腾出了w[i]的空间。同时要加上第i件物品的价值v[i]
2)如果不拿,dp[ i ][ j ] = dp[ i-1 ][ j ] 同3.
至于拿还是不拿,则需要比较1)和2)哪个dp[i][j]比较大
- 可以进行空间优化,即将二维数组转换为一维数组 dp[j]=dp[j-w[i]]+v[i]、dp[j]=dp[j]
由此可以得到状态转移方程:
if(j>=w[i]){
int value1=dp[i-1][j];
int value2=dp[i-1][j-w[i]]+v[i];
if(value1>value2)
dp[i][j]=dp[i-1][j];
else
dp[i][j]=dp[i-1][j-w[i]]+v[i];
}
else
m[i][j]=m[i-1][j];
示例
- 如下图,是商店对五件商品(第0、1、2、3、4件)的价值与重量的表格,你有一个容量c=20,的背包,现在你想在容量内尽可能拿价值更高的商品。
- 此题的状态转移方程如下:
- 具体分析如下(逆序):
- 我们可以建立一个二维数组dp[][]来存储信息,下图以表格形式存储的信息如下:
代码
#include<stdio.h>
int dp[1005][1005];
int s,v[1005],w[1005],n;
int main() {
int T,i,j;
scanf("%d", &T);//输入循环次数,即几组测试样例
while (T--) {
scanf("%d %d", &n, &s);//输入商品个数和背包容量
for (i = 0; i <= n; i++) {
for (j = 0; j <= s; j++) {
dp[i][j]=0;
}
v[i]=0;c[i]=0;
}
for (i = 1; i <= n; i++) {
scanf("%d", &v[i]);//一维数组存储商品的价值
}
for (i = 1; i <= n; i++) {
scanf("%d", &w[i]);//一维数组存储商品的重量
}
for (i = 1; i <= n; i++) {
for (j = s; j >= 0; j--) {//逆序
if (c[i]>j)//如果商品重量大于当前背包容量,肯定不能拿
dp[i][j] = dp[i-1][j];
else{//如果未超重,则看想不想拿,哪个价值大
int value1 = dp[i-1][j-w[i]]+v[i];//拿
int value2 = dp[i-1][j];//不拿
if(value1>value2)
dp[i][j]=value1;
else
dp[i][j]=value2;
}
}
}
printf("%d\n", dp[n][s]);//输出最大价值
}
return 0;
}
- 运行结果
- 如果想知道拿哪几样东西可以获得最大价值
- 另起一个 x[ ] 数组,x[i]=0表示不拿,x[i]=1表示拿。dp[n][s]为最优值,如果dp[n][s]=dp[n-1][s] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则 x[n]=1。当x[n]=0时,由x[n-1][s]继续构造最优解;当x[n]=1时,则由x[n-1][s-c[i]]继续构造最优解。以此类推,可构造出所有的最优解。
- 代码如下:
#include<stdio.h>
int dp[1005][1005];
int x[1005],n,s,v[1005],w[1005];
void traceback()
{
for(int i=n;i>1;i--)
{
if(dp[i][s]==dp[i-1][s])
x[i]=0;
else
{
x[i]=1;
s-=w[i];
}
}
x[1]=(dp[1][s]>0)?1:0;
}
int main() {
int T,i,j;
scanf("%d", &T);
while (T--) {
scanf("%d %d", &n, &s);
for (i = 0; i <= n; i++) {
for (j = 0; j <= s; j++) {
dp[i][j]=0;
}
v[i]=0;c[i]=0;
}
for (i = 1; i <= n; i++) {
scanf("%d", &v[i]);
}
for (i = 1; i <= n; i++) {
scanf("%d", &w[i]);
}
for (i = 1; i <= n; i++) {
for (j = s; j >= 0; j--) {
if (c[i]>j)
dp[i][j] = dp[i-1][j];
else{
int value1 = dp[i-1][j-w[i]]+v[i];
int value2 = dp[i-1][j];
if(value1>value2)
dp[i][j]=value1;
else
dp[i][j]=value2;
}
}
}
printf("%d\n", dp[n][s]);
traceback();
for(int i=1;i<=n;i++)
printf("%d ",x[i]);
}
return 0;
}
- 运行结果
为什么01背包的内层循环是逆序
- dp[i][j]只与dp[i-1][j]和dp[i-1][j-w[i]]有关,即只和i-1时刻状态有关,所以我们只需要用一维数组dp[]来保存i-1时的状态f[]。
假设i-1时刻的dp[]为{a0,a1,a2,…,av},难么i时刻的dp[]中第j个应该为max(av,av-w[i]+v[i])即max(dp[j],dp[j-w[i]]+v[i]),
这就需要我们遍历j时逆序遍历,这样才能保证求i时刻dp[j]时dp[j-w[i]]是i-1时刻的值。如果正序遍历则当求dp[v]时,其前面的dp[0],dp[1],…,dp[j-1]都已经改变过,里面存的都不是i-1时刻的值,这样求dp[j]时利用dp[j-w[i]]必定是错的值。最后dp[j]即为最大价值
具体请参考:https://blog.csdn.net/xiajiawei0206/article/details/19933781