问题&&题目分析
假设有n个物品和1个背包,每个物品的重量为wi,价值为vi,每个物品只有1件,要么装入,要么不装,不可拆分,背包载重量一定,如何装使背包装入的物品价值最高?
算法
这部分有参考《趣学算法》,也有很多自己的理解和思考,如有bug,欢迎批评指正(鞠躬)。
动态规划
之前有写过类似的最优装载问题,那个题目用的是贪心算法,但是这个题目与之前题目的很大不同之处就是这里的物品不能拆分,也就是说我们不能只装一种物品的一部分,这样就不能使用贪心算法。因为使用贪心算法不能保证得到的解是最优解,可能别的装法会比贪心得到的解更优。这个题目呢,跟之前的最长公共子序列问题,编辑距离等问题有一个共同点,那就是它们都有最优子结构,就是说整个问题的最优解会包含子问题的最优解,这个用反证法很容易得证(如果感兴趣可移步前两类问题)。基于0-1背包问题的这个特点,我们可以使用动态规划来解决。
算法核心
我们还是先求子问题的最优解,然后通过递推公式得到较大子问题的最优解,最后得到整个问题的最优解。
c[i][j]:有前i个物品,背包容量为j时所能装载物品的最大价值;
w[i]:第i个物品的重量;
v[i]:第i个物品的价值;
从只有1个物品开始,求出容量为1,2,3,4…,m时的最大价值;然后求2个物品时的…直至最后求到第n个物品,c[n][m]就是所求问题的解。这里最重要的当然是递推公式咯。
if j<w[i],c[i][j]=c[i-1][j];//此时一定不能装入
if j>=w[i],c[i][j]=max(c[i-1][j],c[i-1][j-w[i]]+v[i]);//此时可以装入,就看是装入后的总价值高还是不装的总价值高
这样得到子问题的最优解后,通过递推公式就可以得到整个问题的最优解。
算法流程
用c[][]数组存储子问题的最优解,先将第一行和第一列初始化为0,然后设双重遍历利用递推公式计算数组中的值,这样我们就可以得到背包中装载物品的最大价值。下面就是求出怎么装的,也就是求出背包中装的是什么,设x[]储存回溯结果,就是从c[n][m]开始,如果c[i][j]>c[i-1][j],也就是这个物品装入了背包,那么x[i]=1,否则x[i]=0,因为从递推公式中我们可以看出c[i][j]的值最小就是c[i-1][j],所以只有比c[i-1][j]大的时候才是装入了背包。还有一点要注意的是确定了一个物品放入了背包就要j-=w[i],把这个物品去掉再继续回溯。最后遍历x[]数组,当x[i]==1的时候把下标输出即可。
代码实现
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn=105;
int c[maxn][maxn];//储存子问题的解
int w[maxn],v[maxn];
int x[maxn];//回溯,储存结果
int n;//物品个数
int m;//背包载重量
void cal()//计算背包可装入物品的最大价值
{
int i,j;
for(i=1; i<=n; ++i)
for(j=1; j<=m; ++j)
{
if(j>=w[i])//这个物品在不考虑其他物品的情况下可以装入,有装入的可能性
c[i][j]=max(c[i-1][j],c[i-1][j-w[i]]+v[i]);//是装入还是不装入的价值更高
else
c[i][j]=c[i-1][j];
}
}
void ans()//回溯计算装入背包的物品
{
int i;
int j=m;
for(i=n; i>0; --i)
{
if(c[i][j]>c[i-1][j])//说明这个物品装入了
{
x[i]=1;
j-=w[i];
}
else
x[i]=0;
}
}
int main()
{
memset(c,0,sizeof(c));
memset(w,0,sizeof(w));
memset(v,0,sizeof(v));
memset(x,0,sizeof(x));
cout<<"请输入物品个数:";
cin>>n;
cout<<"请输入背包载重量:";
cin>>m;
cout<<"请依次输入物品的重量和价值:";
int i;
for(i=1; i<=n; ++i)
cin>>w[i]>>v[i];
cal();
cout<<"背包可装入物品的最大价值是:"<<c[n][m]<<endl;
ans();
cout<<"装入的物品为:";
for(i=1; i<=n; ++i)
if(x[i])
cout<<i<<" ";
cout<<endl;
return 0;
}
优化
其实开一个这么大的数组只为求出可装载物品价值的最大值是有点浪费的,如果说只想得到最大值那个值而可以不求装那些物品的话,以下给出一种优化方法。就是把之前的c[maxn][maxn]数组改为d[maxn],这样一来节省了很多空间,但是就不能回溯去求到底取了哪些物品,而且在循环的时候要特别特别小心,稍有不慎就会写错(比如我就写错了)。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn=105;
int d[maxn];//储存子问题的解
int w[maxn],v[maxn];
//int x[maxn];//回溯,储存结果
int n;//物品个数
int m;//背包载重量
void cal()//计算背包可装入物品的最大价值
{
int i,j;
for(i=1; i<=n; ++i)
for(j=m; j>0; --j)//一定要注意,这里是从m开始,因为这样才能保证d[i]和d[i-w[i]]是之前意义上的c[i-1][j],c[i-w[i]][j]
if(j>=w[i])
d[j]=max(d[j],d[j-w[i]]+v[i]);
}
int main()
{
memset(d,0,sizeof(d));
memset(w,0,sizeof(w));
memset(v,0,sizeof(v));
cout<<"请输入物品个数:";
cin>>n;
cout<<"请输入背包载重量:";
cin>>m;
cout<<"请依次输入物品的重量和价值:";
int i;
for(i=1; i<=n; ++i)
cin>>w[i]>>v[i];
cal();
cout<<"背包可装入物品的最大价值是:"<<d[m]<<endl;
return 0;
}