01背包问题求具体方案
问题重述
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 1…N。
数据范围
0<N,V≤1000
0<vi,wi≤1000
思路分析:
从 01 背包那我们已经知道了,这个问题最为核心的状态转移方程是 f[ i ][ j ] = max( f[ i -1][ j ] , f[ i-1 ][ j -v[i]]+wi ), 其表达的意思就是在当前物品选与不选的两种状态选择其中较大的那种,这样我们就能从中分离出一个判断式,即我们可以判断出当前物品选没选,如果选了或者说当前物品可以选(考虑到两者相同的情况)那么它就一定满足 f[ i ][ j ] == f[ i-1 ][ j -v[i]]+wi 。
回到本题我们需要确定我们的最优解选了那些物品,根据上述分析我们就可以在 01 背包的最优解求出来后然后从 N 开始倒退检查第 i 件物品可不可选,如果可选那么我们就将其选定,然后减小体积继续判断,总结就是编号大的物品能选就选。这样我们得到的方案物品编号就是字典序最大的因为我们选的是物品编号尽量大的物品。
然而题目中让我们选出的是字典序最小的方案数,那么目前我们的解是不符合题意的。至此我们思考为什么我们只能选出物品编号尽量大的物品,因为我们的最优解是在判断完最后一个物品才得出的,而我们只有在拿到最优解之后,才能去判断哪些物品可选。所以我们要想得到字典序最小的选择序列,就必须使得最优解是在判断完第一个物品后得出的,那么我们必须扭转 01 背包求解的思想,即判断后从第 N 个物品向前开始判断后几个物品在不同体积下的最优解是什么,最终得到后 N 个物品在体积 V 下的最优解。这样我们就可以在判段判断完第一个物品后得到最优解,再用上述方式就能得到字典序最小的选择方案。因为我们需要确定每一个物品选没选所以每步的信息都需要保留,因此不能优化到一维求解,也不能现场输入物品的体积和价值,只能用最初的求解方式。
C++代码:
#include<iostream>
using namespace std;
int volume[1010],value[1010];//定义储存物品体积和价值的数组
int ans[1010][1010];//定义储存答案的数组
int N,V;
int main()
{
cin>>N>>V; //输入物品数和背包体积
for(int i=1;i<=N;i++)//输入各个物品的属性
{
cin>>volume[i]>>value[i];
}
for(int i=N;i>=1;i--) //从后往前遍历物品求后几个物品的最优解
{
for(int j=0;j<=V;j++)//因为数组是二维的所以体积怎样遍历都可以
{
ans[i][j]=ans[i+1][j];
if(j>=volume[i])
ans[i][j]=max(ans[i][j],ans[i+1][j-volume[i]]+value[i]);//状态转移方程
}
}
int nowvolume=V;//记录当前体积
for(int i=1;i<=N;i++)//从第一个物品开始遍历
{
if( volume[i]<=nowvolume && ans[i][nowvolume]==ans[i+1][nowvolume-volume[i]]+value[i] )
{//判段这个物品是否可选能选就选尽量选小同时要确保当前背包体积能装下此物品
cout<<i<<" ";//输出物品编号
nowvolume-=volume[i];//选择一个物品当前体积要相应减少
}
}
return 0;
}