综合设计性实验报告背包问题的多种算法设计与分析.docx
《综合设计性实验报告背包问题的多种算法设计与分析.docx》由会员分享,可在线阅读,更多相关《综合设计性实验报告背包问题的多种算法设计与分析.docx(42页珍藏版)》请在冰豆网上搜索。
综合设计性实验报告背包问题的多种算法设计与分析
0-1背包问题的多种算法设计与分析
一、实验内容和要求:
0-1背包问题是一例典型的组合优化的NP完全问题。
问题可以描述为:
给定一组共n个物品,每种物品都有自己的重量wi,i=1~n和价值vi,i=1~n,在限定的总重量(背包的容量C)内,如何选择才能使得选择物品的总价值之和最高。
选择最优的物品子集放置于给定背包中,最优子集对应n元解向量(x1,…xn),xi∈{0或1},因此命名为0-1背包问题。
0-1背包问题是许多问题的原型,但它又是一个NP完全问题。
此实验主要研究和实现n(0<=n<=200)和C(C<=2000,C为整数)都较大的情形,随机产生n个物品的重量向量wi(1<=wi<=100,wi为整数)和价值向量vi(1<=vi<=100,vi为整数)。
0-1背包问题可以用许多方法来求解,有些算法可以得到问题的精确最优解,有些仅能获得一个近似最优解。
本综合设计性实验要求用3种以上的方法求解0-1背包问题,获得精确最优解或近似最优解皆可,并对所采用的多种算法从运行时间、寻找是否为最优解、能够求解的问题规模等方面进行对比和分析。
本课程讲述的算法思想都可以用来求解此问题,甚至本课程未涉及的许多算法也非常适合于求解此问题,学生可以先尝试先用本课程介绍的算法来实现和分析,学有余力或兴趣驱动下可以寻找一些智能算法的资料来试一试。
涉及的方法可以有:
蛮力求解、递归求解、动态规划求解、贪心求解、回溯法求解、广度优先的分支限界法求解,优先队列的启发式分支限界法、遗传算法、模拟退火算法、蚁群算法、粒子群算法等。
为方便调试,采用文件输入,标准输出(或文件输出也可)的形式。
数据输入的格式如下:
每组测试数据包含n+1行,第1行为C和n,表示背包容量为C且有n个物品,接下来n行为这n个物品的重量wi和价值vi。
背包容量和物品重量都为整数。
n,C,wi,vi范围如上所述。
输出两行。
第一行为所选物品的最大价值之和,第二行为装入背包的物品所对应的n元最优解向量(x1,…xn),xi∈{0或1}。
二、多种算法详细设计
1.贪心算法
输入物品价值和重量。
分别用数组v[o..n]和w[0..n]来记录。
根据物品价值v[i]/w[i]由高到低对v[o..n]和w[0..n]进行排序。
然后把物品从1到n依次加入背包,直到装不下为止。
设c为背包容量,每次装一个物品,则c-=w[i],当前价值为value+=v[i],当c<=0时,停止加入物品,此时的value即为最优值。
对于背包问题,贪心选择最终可得到最优解,但对于0-1背包问题,贪心选择无法保证最终能将背包装满,部分闲置的背包空间使单位背包空间的价值降低了。
2.递归求解
1)0-1背包最优问题具有最优子结构性质。
设(y1,y2,...,yn)是所给0-1背包的一个最优解,则(y2,y3,...,yn)是除了y1外的最优解。
2)设所给0-1背包问题的子问题的最优值为m(i,j),即m(i,j)是背包容量为j,可选物品为i,i+1,...,n时0-1背包问题的最优值。
由0-1背包的问题的最优子结构性质,可以建立计算m(i,j)的递归式如下:
m(i,j)=max{m(i+1,j),m(i+1,j-wi)+vi}j>=wi
=m(i+1,j)0<=jm(i,j)=vnj>=wn
=00<=j用二维数组m[i][j]存储m(i,j)的值。
根据递归式算出m[i][j]的值。
则m[1][c]是给出所要求的0-1背包问题的最优值。
相应的最优解可由算法Traceback计算如下。
如果m[1][c]=m[2][c],则x1=0,否则x1=1.当x1=0时,由m[2][c]继续构造最优解。
当x1=1时,由m[2][c]继续构造最优解。
当x1=1时,由m[2][c-w1]继续构造最优解。
依次类推,可构造出相应的最优解(x1,x2,...,xn)。
voidTraceback(int**m,intn,int*w,intc,int*x)
{
inti;
if(m[1][c]==m[2][c])
x[1]=0;
elsex[1]=1;
for(i=1;i<=n-2;i++)
{
if(x[i]==1)
c-=w[i];
if(m[i+1][c]==m[i+2][c])
x[i+1]=0;
elsex[i+1]=1;
}
if(m[n][c]!
=0)
x[n]=1;
}
3.动态规划
基于递归式的讨论,用二维数组m[][]来存储m(i,j)的相应值,可设计解0-1背包问题的动态规划算法Knapsack如下:
template
voidKnapsack(Type*v,int*w,intc,intn,Type**m)
{
intjMax=min(w[n]-1,c);
for(intj=0;j<=jMax;j++)m[n][j]=0;
for(j=w[n];j<=c;j++)m[n][j]=v[n];
for(inti=n-1;i>1;i--){
jMax=min(w[i]-1,c);
for(j=0;j<=jMax;j++)m[i][j]=m[i+1][j];
for(j=w[i];j<=c;j++)m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
m[1][c]=m[2][c];
if(c>=w[1])m[1][c]=max(m[1][c],m[2][c-w[1]]+v[1]);
}
template
voidTraceback(Type**m,int*w,intc,intn,int*x)
{
fpr(inti=1;iif(m[i][c]==m[i+1][c])x[i]=0;
else{x[i]=1;c-=w[i];}
x[n]=(m[n][c])?
1:
0;
}
4.回溯法求解
0-1背包问题是子集选取问题。
一般情况下,0-1背包问题是NP难的。
0-1背包问题的解空间可用子集树表示。
在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入其左子树。
当右子树中有可能包含最优解时才进入右子树搜索。
否则将右子树剪去。
设r是当前剩余物品价值总和;cp是当前价值;bestp是当前最优价值。
计算右子树上界的方法是将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下去时,再装入该物品的一部分而装满背包。
由此得到的价值是右子树中解的上界。
为了计算上界,可先将物品依其单位重量价值从小到大排序,此后只要按顺序考察各物品即可。
将物品依其单位价值排序的算法如下:
classObject{
friendintKnapsack(int*,int*,int,int,int*);
friendvoidSort(Object*,int);
public:
intoperator<=(Objecta)const
{return(d>=a.d);}
private:
intID;
floatd;//单位重量价值
};
voidSort(Object*Q,intlen)
{
for(inti=0;ifor(intj=0;jif(Q[i]<=Q[j])
{
Objecttemp=Q[i];
Q[i]=Q[j];
Q[j]=temp;
}
}
在实现时,由Bound计算当前结点处的上界。
类Knap的数据成员记录解空间树中的结点信息,以减少参数传递及递归调用所需的栈空间。
在解空间树的当前扩展结点处,仅当要进入右子树时才计算上界与其父节点的上界Bound,以判断是否可将右子树剪去。
进入左子树时不需要计算上界,因为其上界与其父结点的上界相同。
在计算最优值的时候也要在另设一个数组bestx[1..n]来记录最优解,在Backtrack算法中回溯得到bestx,当得到第一个最优值,若后面回溯到叶结点时值不大于当前最优值,则最优解bestx不改变。
Backtrack中得到最优值和最优解的算法如下:
template
voidKnap:
:
Backtrack(inti)
{
intflag;
if(i>n){//到达叶节点
flag=bestpif(flag)
{bestp=cp;
for(i=1;i<=n;i++)
bestx[ID[i]]=x[ID[i]];
}
return;}
if(cw+w[i]<=c){//进入左子树
cw+=w[i];
cp+=p[i];
x[ID[i]]=1;
Backtrack(i+1);
cw-=w[i];
cp-=p[i];
}
if(Bound(i+1)>bestp)//进入右子树
{
x[ID[i]]=0;
Backtrack(i+1);}
}
5.分支界限法
在解0-1背包问题的优先队列式分支限界法中,活结点优先队列中结点元素N的优先级由该结点的上界函数Bound计算出的值uprofit给出。
上界函数与回溯法一样。
子集树中以结点N为根的子树中任一结点的价值不超过N.profit。
可用一个最大堆来实现活结点优先队列。
堆中元素类型为HeapNode。
其私有成员有uprofit,profit,weight和level。
对于任意活结点N,N.weight是结点N所相应的重量;N.profit是N所相应的价值;N.uprofit是结点N的价值上界,最大堆以这个值作为优先级。
子集空间树中结点类型为bbnode。
类Knap的成员bestx用来记录最优解。
Bestx[i]=1当且仅当最优解含有物品i。
算法MaxKnapsack实施对子集树的优先队列式分支限界搜索。
算法中E是当前扩展结点;cw是该结点所相应的重量;cp是相应的价值;up是价值上界。
算法的while循环不断扩展结点,直到子集树的叶结点成为扩展结点时为止。
此时优先队列中所有结点的价值上界均不超过该结点的价值。
因此该叶结点相应的解为问题的最优解。
在while循环内部,算法首先检查当前扩展结点的左儿子结点的可行性。
如果该左儿子结点是可行结点,则将它加入到子集树和活结点优先队列中。
当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界约束时才将它加入子集树和活结点优先队列。
建立最大堆MaxHeap,并对HeapNode类中做相应修改,目的是为了使最大堆能根据N.uprofit值作相应调整。
修改如下:
template
classHeapNode{
friendKnap;
public:
intoperator<(HeapNodea)const{returnuprofitintoperator>(HeapNodea)const{returnuprofit>a.uprofit;}
private:
Typepuprofit,//结点的价值上界
profit;//结点所相应的价值
Typewweight;//结点所相应的重量
intlevel;//活结点在子集树中所处的层序号
bbnode*ptr;//指向活结点在子集树中相应结点的指针
};
3、多种算法调试和测试
1.贪心算法
对于背包问题,贪心选择最终可得到最优解,但对于0-1背包问题,贪心选择无法保证最终能将背包装满,部分闲置的背包空间使单位背包空间的价值降低了。
测试中数据基本与最优值相似,有的则相差较大。
贪心算法得出的结不一定是最优解。
2.递归求解
在编写递归的算法,最关键的就是怎样用二维数组来表示相应最优解的值,编写的时候需要注意传值。
思路清晰,把数学的递归式用代码函数表示出来。
对于递归求解,算法简单,但消耗时间太长。
在数据较少的情况下,可以很快得到正确结果,当数据达到35个数据的时候,需要较长时间,当数据达到40个以上的时候,由于时间太长则等待不出结果。
可见递归的效率很低。
针对递归算法效率较低,可采用动态规划进行改进。
3.动态规划
采用动态规划算法,效率较高。
用所提供的数据测试,均能很快得出正确结果。
在编写动态规划算法时,没有太大问题,只需要编写求出最大值和最小值的函数。
还有回溯求最优解的算法。
4.回溯法求解
在编写该算法时首先遇到的第一个问题是要运用C++的一些语法,这对于只懂C的同学确实存在一下障碍。
好在我们的小组成员有些人之前学过,所以在这方面问题不大,再加上不懂时通过网上查阅资料和相关书籍,问题很快就得到解决。
另外,就是在Backtrack算法中求出最优值和最优解,每次当剩余的价值大于bestp,则进入右子树进行搜索。
当到达叶结点时即得到一个最优值,把值赋给bestp,得到新的最优值。
同时需记录最优解,此时把记录路径的数组x[1..n]的值赋给bestx[1..n]。
在记录最优解时需要注意一个问题。
因为回溯的顺序是按照物品单位价值从高到低的顺序进行的,而输出的最优解则需要按照原顺序输出。
所以在回溯中记录路径时,需注意记录的是原来次序下的路径,而不是排序后的路径。
在这里我们在排序之前已经定义好一个类Q。
类中的数据成员ID用来记录物品原来的位置。
则排序后,物品原来的位置记录仍能被找到。
则搜索到某个结点时,则x[ID[i]]=1,否则x[ID[i]]=0.
5.分支界限法
分支限界法的与回溯法不同的是前者是深度优先搜索,后者是广度优先搜索。
分支限界法的效率会高些,但是需要建立一个最大堆作为优先队列。
定义一个类MaxHeap来实现优先队列,定义如下:
template
classMaxHeap
{
public:
MaxHeap(Ta[],intsize,intmaxsize=50);
MaxHeap(intmaxsize=50);
virtual~MaxHeap();
voidInitialize(Tarrays[],intsize,intarray_size);//用数组对堆重新进行初始化
MaxHeap&Insert(Tvalue);
MaxHeap&DeleteMax(T&value);
boolIsEmpty(){returnCurrentSize==0?
true:
false;};
intSize(){returnCurrentSize;}
voidInit(Ta[]);
private:
voidModifyUp(intstart);
voidModifyDown(intstart,intend);
private:
T*Data;
intMaxSize;
intCurrentSize;
};
调试后小结:
从编程到调试到可以运行中总会遇到各种各样的问题。
在本次综合实验中我们一会颇深,在试验中我们学到了新知识,一些C++的语法,一些调试的技巧,以及根据纠错提示改错。
同时也巩固了一些旧的知识,如文件的读写操作,输入输出。
先把总结如下:
(1)C++的语法,定于各种类,增加了程序可读性,是程序比较容易理解。
同时在类中定义友元类或者友元函数,使类之间的某些操作不受限制,方便程序员的编写,完善功能。
另外,类中的函数可在类中先申明,然后类在外定义,也是非常方便的。
还有运算符的重载,以及类模板,函数模板的使用都增加了程序的可读性和易编写性。
(2)说来有点惭愧,在这次综合实验中,才真正学会VC的单步调试和断点,这给我们编写程序带来极大便利。
另外通过查看每歩程序的输出,也可以方便的找出错误。
(3)这里是一些纠错提示:
1.C程序编译时出现warnningnonewlineatendoffile。
解决办法:
在文件最后添加一个空白行。
2.LINK:
fatalerrorLNK1168:
cannotopenDebug/Test.exeforwriting
解决办法:
打开任务管理器,将test.exe进程杀掉,然后重新编译链接
(4)数据的格式读入,由于数据很多,所以采用文件输入比较方便。
FILE*fp;
fp=fopen("D:
\\bag.txt","r");//以读的方式打开文件
if(!
fp)
{
printf("filecannotbeopened");
exit
(1);
}
fscanf(fp,"%Ld%Ld",&c,&n);
数据的文件格式输出
FILE*ptr;
ptr=fopen("D:
\\result.txt","w");//以写的方式打开文件
fprintf(ptr,"%d\n",value);
for(i=1;i<=n;i++)
fprintf(ptr,"%d%d\n",i,x[i]);
最后还要关闭文件。
fclose(fp);
fclose(ptr);
4、多种算法对比
算法
运行时间/s(输入数据个数)
是否为最优解
能够求解问题规模
20
30
40
100
200
1.贪心算法
0
0
0
0
0
否
--
2.递归求解
0
0.156
14.078
0
0
是
40+
3.动态规划
0
0
0
0.015
0.015
是
--
4.回溯法
0
0
0
0
0.015
是
--
5.分支限界法
0
0
0
0
0
是
--
五、附录多种算法实现清单:
带注释和功能模块说明的源程序清单:
1.贪心算法
#include
usingnamespacestd;
voidsort2(intn,int*v,int*w,int*&sort)
{
inti,j,temp;
for(i=2;i<=n;i++)
{
for(j=1;j<=i;j++)
if(v[sort[i]]/w[i]{
temp=sort[i];sort[i]=sort[j];sort[j]=temp;
}
}
}
intGreedy(intn,int*w,int*v,int*&x,intc,int*sort)
{
inti,value=0;
for(i=n;i>=1;i--)
{
c-=w[sort[i]];
if(c>=0)
{x[sort[i]]=1;value+=v[sort[i]];}
elsebreak;
}
returnvalue;
}
intmain()
{
intn,c,i,value;
FILE*fp;
FILE*ptr;
int*x,*w,*v,*sort;
fp=fopen("D:
\\bag.txt","r");//以读的方式打开文件
if(!
fp)
{
printf("filecannotbeopened");
exit
(1);
}
fscanf(fp,"%Ld%Ld",&c,&n);
x=newint[n+1];
w=newint[n+1];
v=newint[n+1];
sort=newint[n+1];
for(i=1;i<=n;i++)
{sort[i]=i;x[i]=0;}
for(i=1;i<=n;i++)
fscanf(fp,"%Ld%Ld",&w[i],&v[i]);
sort2(n,v,w,sort);//从小到大
value=Greedy(n,w,v,x,c,sort);//调用函数
ptr=fopen("D:
\\result.txt","w");//以写的方式打开文件
fprintf(ptr,"%d\n",value);
for(i=1;i<=n;i++)
fprintf(ptr,"%d%d\n",i,x[i]);
fclose(fp);
fclose(ptr);
return0;
}
2.递归求解
#include
usingnamespacestd;
intma(inti,intj,intn,int**&m,int*w,int*v)
{
inta,b,value;
if(i==n)
{
if(j>=w[n])
value=v[n];
elsevalue=0;
returnvalue;
}
if(i{if(j>=w[i])
{
m[i+1][j]=ma(i+1,j,n,m,w,v);
a=m[i+1][j];
m[i+1][j-w[i]]=ma(i+1,j-w[i],n,m,w,v);//递归的调用
b=m[i+1][j-w[i]]+v[i];
if(a>b)
value=a;
elsevalue=b;
returnvalue;
}
else
{m[i+1][j]=ma(i+1,j,n,m,w,v);//递归的调用
value=m[i+1][j];
returnvalue;
}
}
else
return0;
}
voidTraceback(int**m,intn,int*w,intc,int*x)
{
inti;
if(m[1][c]==m[2][c])
x[1]=0;
elsex[1]=1;
for(i=1;i<=n-2;i++)
{
if(x[i]==1)
c-=w[i];
if(m[i+1][c]==m[i+2][c])
x[i+1]=0;
elsex[i+1]=1;
}
if(m[n][c]!
=0)
x[n]=1;
}
intmain()
{
intn,c,i;
FILE*fp;
FILE*ptr;
int*x,*w,*v,**m;
fp=fopen("D:
\\bag.txt","r");//以读的方式打开文件
if(!
fp)
{
printf("filecannotbeopened");
exit
(1);
}
fscanf(fp,"%Ld%Ld",&c,&n);
x=newint[n+1];
w=newint[n+1];
v=newint[n+1];
m=newint*[n+1];
for(i=1;i<=n;i++)
{
m[i]=newint[c+1];
x[i]=0;
fscanf(fp,"%Ld%Ld",&w[i],&v[i]);
}
for(i=1;i<=c;i++)
{
if(i>=w[n])
m[n][i]=v[n];