问题描述
0/1背包问题物品集合U={u1,u2…un},体积分别为s1,s2…..sn,价值分别为v1,v2….vn;容量C的背包。设计算法实现放入背包的物品价值最大。
输入描述
第一行输入物品数n和背包的容量C,接下来n行分别输入每个物品体积及价值
输出描述
输出最大价值数
输入样例
3 10
3 4
4 5
5 6
输出样例
11
现在要求给出一种最优装载方案x,满足总容量<=C,使得总价值最大,那么考虑前i个物品1,2,3~~i,以及背包容量j,
3 10
3 4
4 5
5 6
输出样例
11
问题分析
每个物品有装入和不装入两个选择,定义n维向量x=(x1,x2,x3,~~xn),其中xk=1表示物品k装入,xk=0表示不装入,因此一个0-1向量代表了一种装载方案,现在要求给出一种最优装载方案x,满足总容量<=C,使得总价值最大,那么考虑前i个物品1,2,3~~i,以及背包容量j,
记D[i][j]为前i个物品在背包容量为j的情况下所能得到的最大价值。
它满足以下递推关系:
(1) D[0][j]=0,j=0,1,2,,C; 物品为0时肯定最大价值全为0 这是递推起点 需要初始化
(2) 若j<w[i],D[i][j]=D[i-1][j],否则D[i][j]=max(D[i-1][j],D[i-1][j-w[i]]+v[i]); 这是递推表达式
它满足以下递推关系:
(1) D[0][j]=0,j=0,1,2,,C; 物品为0时肯定最大价值全为0 这是递推起点 需要初始化
(2) 若j<w[i],D[i][j]=D[i-1][j],否则D[i][j]=max(D[i-1][j],D[i-1][j-w[i]]+v[i]); 这是递推表达式
D[i][j]的计算要用到上一行的两个元素,因此计算顺序和填二维数组的顺序是一样的,从左往右,再从左往右
则最后右下角的D[n][C]就是最大价值
代码展示
#include<iostream>
using namespace std;
#define Max 1000
#define N 20
#define max(a,b) a>b?a:b
int D[Max][Max];
int Fill(int n,int C,int w[],int v[])
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=C;j++)
{
if(j<w[i])//背包剩余容量小于当前物品容量,即装不下
D[i][j]=D[i-1][j];
else //能够装下,就要做选择,装还是不装
D[i][j]=max(D[i-1][j],D[i-1][j-w[i]]+v[i]);//反正总共只有j的容量 就在装与不装中选择最大价值的情况
} //D[i-1][j]是不放入时的价值 D[i-1][j-w[i]]+v[i]是放 入时,前i-1个物品的价值加上当前物品的价值
}
return D[n][C];
}
int main()
{
int w[N],v[N],C,n;
cin>>n>>C;//输入物品个数及背包总容量
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];//输入每个物品的容量及价值
for(int i=0;i<=C;i++)
D[0][i]=0;//初始化二维数组 其实这个for循环不写也没关系,因为D是全局数组,系统自动全初始化为0
// Fill(n,C,w,v);//可以进行直接调用
cout<<Fill(n,C,w,v)<<endl;//进行参数传递
cout<<D[n][C]<<endl;//这两行均输出 前n个物品且背包容量为C的情况下,背包所能装入物品的最大价值
return 0;
}
那么根据这个样例列出的表如下:以物品个数i为纵坐标,背包容量j为横坐标,表中数据即为前i个物品在背包容量为j时的情况下所能得到的最大价值。
| i⁄j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
| 2 | 0 | 0 | 0 | 4 | 5 | 5 | 5 | 9 | 9 | 9 | 9 | 9 | 9 |
| 3 | 0 | 0 | 0 | 4 | 5 | 6 | 6 | 9 | 10 | 11 | 11 | 11 | 15 |
这里背包最大容量是10,其实只要列到10就好了,但是为了方便理解,这里列到12是把所有物品的容量都加起来了。其实这个表是把所有可能的情况列出来了,
单独一个或者两个,或者三个物品放入。
最优装载方案
最优装载方案即x=(x1,x2,x3,~~xn),可以从D中得到,从右下角的D[n][C]开始往前回溯,根据递推关系(2),如果D[i][j]==D[i-1][j],说明xi=0,否则xi=1。
int Optload(int n,int C,int w[],int v[],int X[])
{
int i,j;
i=n;
j=C;
while(i>0)
{
if(D[i][j]==D[i-1][j])
{
X[i]=0;
i--;
}
else
{
X[i]=1;
j-=w[i];
i--;
}
}
return D[n][C];
}
上述是用的二维数组
接下来用一维数组来解决这个问题,注意一下内层循环要逆序
#include<iostream>
using namespace std;
#define Max 1000
#define N 20
#define max(a,b) a>b?a:b
int D[Max];//这里系统已经把数组全初始化为0了,相当于递推起点
int Fill(int n,int C,int w[],int v[])
{
for(int i=1;i<=n;i++)//每一次外循环都会更新一维数组里的数据
{
// 这里j要逆序因为如果正序遍历,则前面的值将有可能被修改掉从而造成后面数据的错误; 因
// 为后面的值是由前面的值构成的,可以自己打表来验证一下。 // 相反如果逆序遍历,先修改后面的数据再修改前面的数据,此种情况就不会出错了。
// 我们可以发现, 在一维递归式里, 要求D[j-wi]+vi 这部分代替二维中的 D[i-1][j-wi]+vi
// 这部分我们现在又只有一维数组. 这就要保证, 在第i次外循环时,
// D[j]调用的D[j-wi]实际上是基于第i-1次循环得到的值.而逆序能保证
// 在第i次循环时D[j-wi]后于D[j]更新,保证D[j]得到的值是准确的
// 同理,二维数组中不需要逆序是因为for循环直接在数组中填数据,并没有像一维那样每次循环要覆盖才能填入
for(int j=C;j>=w[i];j--) //这里j>=w[i]是因为j<w[i]会装不下,就没有判断的必要了
{
D[j]=max(D[j],D[j-w[i]]+v[i]); //递推表达式
}
// for(int j=C;j>=0;j--) 这个for循环和上面那个是等价的
// { 随便用哪个都行
// if(D[j]<=D[j-w[i]]+v[i]&&j-w[i]>=0)
// D[j]=D[j-w[i]]+v[i];
// }//如果值得改变并且j的空间还装得下就赋新值
}
return D[C];
}
int main()
{
int w[N],v[N],C,n;
cin>>n>>C;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
// Fill(n,C,w,v);
cout<<Fill(n,C,w,v)<<endl;
cout<<D[C]<<endl;
return 0;
}
//然而用一维数组不足的是,虽然优化了动态规划的空间,但是该方法不能找到最优解的解组成,
//因为动态规划寻找解组成一定得在确定了最优解的前提下再往回找解的构成,而优化后的动态规划只用了一维数组,
//每一次循环之前的数据会被覆盖掉,所以没办法寻找,所以两种方法各有其优点。