V[i][j]=V[i-1][j];
else
V[i][j]=max(V[i-1][j],V[i-1][j-a[i-1].w]+a[i-1].p);
j=C;//求装入背包的物品
for(i=n;i>0;i--)
{
if(V[i][j]>V[i-1][j]){
x[i-1]=1;
j=j-a[i-1].w;
}
elsex[i-1]=0;
}
returnV[n][C];//返回背包取得的最大价值
}
intmain()
{
goodsb[N];
printf("物品种数n:
");
scanf("%d",&n);//输入物品种数
printf("背包容量C:
");
scanf("%d",&C);//输入背包容量
for(inti=0;i{
printf("物品%d的重量w[%d]及其价值v[%d]:
",i+1,i+1,i+1);
scanf("%d%d",&a[i].w,&a[i].p);
b[i]=a[i];
}
intsum2=KnapSack2(n,a,C,X);//调用动态规划法求0/1背包问题
printf("动态规划法求解0/1背包问题:
\nX=[");
for(i=0;icout<printf("]装入总价值%d\n",sum2);
for(i=0;i{
a[i]=b[i];
}//恢复a[N]顺序
}
3)复杂度分析:
动态规划法求解0/1背包问题的时间复杂度为:
。
3.回溯法求解0/1背包问题:
1)基本思想:
回溯法:
为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(boundingfunction)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。
这种具有限界函数的深度优先生成法称为回溯法。
对于有n种可选物品的0/1背包问题,其解空间由长度为n的0-1向量组成,可用子集数表示。
在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入左子树。
当右子树中有可能包含最优解时就进入右子树搜索。
2)代码:
#include
#include
usingnamespacestd;
#defineN100//最多可能物体数
structgoods//物品结构体
{
intsign;//物品序号
intw;//物品重量
intp;//物品价值
}a[N];
boolm(goodsa,goodsb)
{
return(a.p/a.w)>(b.p/b.w);
}
intmax(inta,intb)
{
returna
b:
a;
}
intn,C,bestP=0,cp=0,cw=0;
intX[N],cx[N];
intBackTrack(inti)
{
if(i>n-1){
if(bestPfor(intk=0;kbestP=cp;
}
returnbestP;
}
if(cw+a[i].w<=C){//进入左子树
cw=cw+a[i].w;
cp=cp+a[i].p;
cx[a[i].sign]=1;//装入背包
BackTrack(i+1);
cw=cw-a[i].w;
cp=cp-a[i].p;//回溯,进入右子树
}
cx[a[i].sign]=0;//不装入背包
BackTrack(i+1);
returnbestP;
}
intKnapSack3(intn,goodsa[],intC,intx[])
{
for(inti=0;i{
x[i]=0;
a[i].sign=i;
}
sort(a,a+n,m);//将各物品按单位重量价值降序排列
BackTrack(0);
returnbestP;
}
intmain()
{
goodsb[N];
printf("物品种数n:
");
scanf("%d",&n);//输入物品种数
printf("背包容量C:
");
scanf("%d",&C);//输入背包容量
for(inti=0;i{
printf("物品%d的重量w[%d]及其价值v[%d]:
",i+1,i+1,i+1);
scanf("%d%d",&a[i].w,&a[i].p);
b[i]=a[i];
}
intsum3=KnapSack3(n,a,C,X);//调用回溯法求0/1背包问题
printf("回溯法求解0/1背包问题:
\nX=[");
for(i=0;icout<printf("]装入总价值%d\n",sum3);
for(i=0;i{
a[i]=b[i];
}//恢复a[N]顺序
3)复杂度分析:
最不理想的情况下,回溯法求解0/1背包问题的时间复杂度为:
。
由于其对蛮力法进行优化后的算法,其复杂度一般比蛮力法要小。
空间复杂度:
有
个物品,即最多递归
层,存储物品信息就是一个一维数组,即回溯法求解0/1背包问题的空间复杂度为
。
4.分支限界法求解背包问题:
1)基本思想:
分支限界法类似于回溯法,也是在问题的解空间上搜索问题解的算法。
一般情况下,分支限界法与回溯法的求解目标不同。
回溯法的求解目标是找出解空间中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
首先,要对输入数据进行预处理,将各物品依其单位重量价值从大到小进行排列。
在下面描述的优先队列分支限界法中,节点的优先级由已装袋的物品价值加上剩下的最大单位重量价值的物品装满剩余容量的价值和。
算法首先检查当前扩展结点的左儿子结点的可行性。
如果该左儿子结点是可行结点,则将它加入到子集树和活结点优先队列中。
当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界约束时才将它加入子集树和活结点优先队列。
当扩展到叶节点时为问题的最优值。
2)代码:
#include
#include
usingnamespacestd;
#defineN100//最多可能物体数
structgoods//物品结构体
{
intsign;//物品序号
intw;//物品重量
intp;//物品价值
}a[N];
boolm(goodsa,goodsb)
{
return(a.p/a.w)>(b.p/b.w);
}
intmax(inta,intb)
{
returna
b:
a;
}
intn,C,bestP=0,cp=0,cw=0;
intX[N],cx[N];
structKNAPNODE//状态结构体
{
bools1[N];//当前放入物体
intk;//搜索深度
intb;//价值上界
intw;//物体重量
intp;//物体价值
};
structHEAP//堆元素结构体
{
KNAPNODE*p;//结点数据
intb;//所指结点的上界
};
//交换两个堆元素
voids&a,HEAP&b)
{
HEAPtemp=a;
a=b;
b=temp;
}
//堆中元素上移
voidmov_up(HEAPH[],inti)
{
booldone=false;
if(i!
=1){
while(!
done&&i!
=1){
if(H[i].b>H[i/2].b){
swap(H[i],H[i/2]);
}else{
done=true;
}
i=i/2;
}
}
}
//堆中元素下移
voidmov_down(HEAPH[],intn,inti)
{
booldone=false;
if((2*i)<=n){
while(!
done&&((i=2*i)<=n)){
if(i+1<=n&&H[i+1].b>H[i].b){
i++;
}
if(H[i/2].bswap(H[i/2],H[i]);
}else{
done=true;
}
}
}
}
//往堆中插入结点
voidinsert(HEAPH[],HEAPx,int&n)
{
n++;
H[n]=x;
mov_up(H,n);
}
//删除堆中结点
voiddel(HEAPH[],int&n,inti)
{
HEAPx,y;
x=H[i];y=H[n];
n--;
if(i<=n){
H[i]=y;
if(y.b>=x.b){
mov_up(H,i);
}else{
mov_down(H,n,i);
}
}
}
//获得堆顶元素并删除
HEAPdel_top(HEAPH[],int&n)
{
HEAPx=H[1];
del(H,n,1);
returnx;
}
//计算分支节点的上界
voidbound(KNAPNODE*node,intM,goodsa[],intn)
{
inti=node->k;
floatw=node->w;
floatp=node->p;
if(node->w>M){//物体重量超过背包载重量
node->b=0;//上界置为0
}else{
while((w+a[i].w<=M)&&(iw+=a[i].w;//计算背包已装入载重
p+=a[i++].p;//计算背包已装入价值
}
if(inode->b=p+(M-w)*a[i].p/a[i].w;
}else{
node->b=p;
}
}
}
//用分支限界法实现0/1背包问题
intKnapSack4(intn,goodsa[],intC,intX[])
{
inti,k=0;//堆中元素个数的计数器初始化为0
intv;
KNAPNODE*xnode,*ynode,*znode;
HEAPx,y,z,*heap;
heap=newHEAP[n*n];//分配堆的存储空间
for(i=0;ia[i].sign=i;//记录物体的初始编号
}
sort(a,a+n,m);//对物体按照价值重量比排序
xnode=newKNAPNODE;//建立父亲结点
for(i=0;ixnode->s1[i]=false;
}
xnode->k=xnode->w=xnode->p=0;
while(xnode->kynode=newKNAPNODE;//建立结点y
*ynode=*xnode;//结点x的数据复制到结点y
ynode->s1[ynode->k]=true;//装入第k个物体
ynode->w+=a[ynode->k].w;//背包中物体重量累计
ynode->p+=a[ynode->k].p;//背包中物体价值累计
ynode->k++;//搜索深度++
bound(ynode,C,a,n);//计算结点y的上界
y.b=ynode->b;
y.p=ynode;
insert(heap,y,k);//结点y按上界的值插入堆中
znode=newKNAPNODE;//建立结点z
*znode=*xnode;//结点x的数据复制到结点z
znode->k++;//搜索深度++
bound(znode,C,a,n);//计算节点z的上界
z.b=znode->b;
z.p=znode;
insert(heap,z,k);//结点z按上界的值插入堆中
deletexnode;
x=del_top(heap,k);//获得堆顶元素作为新的父亲结点
xnode=x.p;
}
v=xnode->p;
for(i=0;iif(xnode->s1[i]){
X[a[i].sign]=1;
}else{
X[a[i].sign]=0;
}
}
deletexnode;
deleteheap;
returnv;//返回背包中物体的价值
}
/*测试以上算法的主函数*/
intmain()
{
goodsb[N];
printf("物品种数n:
");
scanf("%d",&n);//输入物品种数
printf("背包容量C:
");
scanf("%d",&C);//输入背包容量
for(inti=0;i{
printf("物品%d的重量w[%d]及其价值v[%d]:
",i+1,i+1,i+1);
scanf("%d%d",&a[i].w,&a[i].p);
b[i]=a[i];
}
intsum4=KnapSack4(n,a,C,X);//调用分支限界法求0/1背包问题
printf("分支限界法求解0/1背包问题:
\nX=[");
for(i=0;icout<printf("]装入总价值%d\n",sum4);
return0;
}
3)复杂度分析:
分支限界法求解0/1背包问题的时间复杂度为:
。
相同的数据,求相同同的问题,用不同的方法,得到的结果,所得结果
正式所求问题的最优解,所编程序是正确的。
五、调试和运行程序过程中产生的问题、采取的措施及获得的相关经验教训:
1.本实验中4种算法要用同一个主函数调用,由于背包容量、物品、解向量等变量都是以全局变量定义的,每次调用一种算法之前,必须要看这些重要的变量值是否因上一个算法的调用而发生了改变,如发生改变,则需将其改回原值,使得各种算法之间互不影响。
2.在本实验中,各种算法因申请存储空间等原因,运行时间的排序不可能与其时间复杂度的一致,甚至可能会有很大差别,又不可能输入大量数据进行各种算法的测试,所以没有求各算法的运行时间并进行比较。
即各算法的运行时间受较多因素影响,较小数据量时与算法时间复杂度无相关性,比较是没有意义的