01背包问的题目四种不同算法地实现.docx
《01背包问的题目四种不同算法地实现.docx》由会员分享,可在线阅读,更多相关《01背包问的题目四种不同算法地实现.docx(41页珍藏版)》请在冰豆网上搜索。
![01背包问的题目四种不同算法地实现.docx](https://file1.bdocx.com/fileroot1/2023-1/5/ee92a1e9-24b7-4945-9546-7b682ba89975/ee92a1e9-24b7-4945-9546-7b682ba899751.gif)
01背包问的题目四种不同算法地实现
兰州交通大学
数理与软件工程学院
题目0-1背包问题算法实现
院系数理院
专业班级信计09
学生姓名雷雪艳
学号200905130
指导教师李秦
二O一二年六月五日
一、问题描述:
1、0—1背包问题:
给定n种物品和一个背包,背包最大容量为M,物品i的重量是wi,其价值是平Pi,问应当如何选择装入背包的物品,似的装入背包的物品的总价值最大?
背包问题的数学描述如下:
2、要求找到一个n元向量(x1,x2…xn),在满足约束条件:
情况下,使得目标函数
,其中,1
i
n;M>0;wi>0;pi>0。
满足约束条件的任何向量都是一个可行解,而使得目标函数达到最大的那个可行解则为最优解[1]。
给定n种物品和1个背包。
物品i的重量是wi,其价值为pi,背包的容量为M。
问应如何装入背包中的物品,使得装人背包中物品的总价值最大?
在选择装人背包的物品时,对每种物品i只有两种选择,即装入背包、不装入背包。
不能将物品i装人背包多次,也不能只装入部分的物品i。
该问题称为0-1背包问题。
0-1背包问题的符号化表示是,给定M>0,wi>0,pi>0,1
i
n,要求找到一个n元0-1向量向量(x1,x2…xn),Xi=0或1,1
i
n,使得
,而且
达到最大[2]。
二、解决方案:
方案一:
贪心算法
1、贪心算法的基本原理与分析
贪心算法总是作出在当前看来是最好的选择,即贪心算法并不从整体最优解上加以考虑,它所作出的选择只是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,但对范围相当广的许多问题它能产生整体最优解。
在一些情况下,即使贪心算法不能得到整体最优解,但其最终结果却是最优解的很好近似解。
贪心算法求解的问题一般具有两个重要性质:
贪心选择性质和最优子结构性质。
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优解的选择,即贪心选择来达到。
这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
2、0-1背包问题的实现
对于0-1背包问题,设A是能装入容量为c的背包的具有最大价值的物品集合,则Aj=A-{j}是n-1个物品1,2,...,j-1,j+1,...,n可装入容量为c-wj的背包的具有最大价值的物品集合。
用贪心算法求解0-1背包问题的步骤是,首先计算每种物品单位重量的价值vi/wi;然后,将物品进行排序,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。
若将这种物品全部装入背包后,背包内的物品总量未超过c,则选择单位重量价值次高的物品并尽可能多地装入背包。
依此策略一直进行下去,直到背包装满为止。
3、算法设计如下:
#include
#definemax100//最多物品数
voidsort(intn,floata[max],floatb[max])//按价值密度排序
{
intj,h,k;
floatt1,t2,t3,c[max];
for(k=0;kc[k]=a[k]/b[k];
for(j=0;jif(c[j]{t1=a[j];a[j]=a[j+1];a[j+1]=t1;
t2=b[j];b[j]=b[j+1];b[j+1]=t2;
t3=c[j];c[j]=c[j+1];c[j+1]=t3;
}
}
voidknapsack(intn,floatlimitw,floatv[max],floatw[max],intx[max])
{floatc1;//c1为背包剩余可装载重量
inti;
sort(n,v,w);//物品按价值密度排序
c1=limitw;
for(i=0;i{
if(w[i]>c1)break;
x[i]=1;//x[i]为1时,物品i在解中
c1=c1-w[i];
}
}
voidmain()
{intn,i,x[max];
floatv[max],w[max],totalv=0,totalw=0,limitw;
cout<<"请输入n和limitw:
";
cin>>n>>limitw;
for(i=1;i<=n;i++)
x[i]=0;//物品选择情况表初始化为0
cout<<"请依次输入物品的价值:
"<for(i=1;i<=n;i++)
cin>>v[i];
cout<cout<<"请依次输入物品的重量:
"<for(i=1;i<=n;i++)
cin>>w[i];
cout<knapsack(n,limitw,v,w,x);
cout<<"theselectionis:
";
for(i=1;i<=n;i++)
{
cout<if(x[i]==1){
totalw=totalw+w[i];
totalv=totalv+v[i];
}
}
cout<cout<<"背包的总重量为:
"<cout<<"背包的总价值为:
"<}
4、贪心算法运行结果如下图所示:
方案二:
动态规划算法
1、动态规划的基本原理与分析
动态规划算法的基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
但是经分解得到的子问题往往不是互相独立的。
不同子问题的数目常常只有多项式量级。
如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
它把已知问题分为很多子问题,按顺序求解子问题,在每一种情况下,列出各种情况的局部解,按条件从中选取那些最有可能产生最佳的结果舍弃其余。
前一子问题为后面子问题提供信息,而减少计算量,最后一个子问题的解即为问题解。
采用此方法求解0-1背包问题的主要步骤如下:
①分析最优解的结构:
最有子结构性质;
②建立递归方程;
③计算最优值;
④构造最优解[4]。
2、0-1背包问题的实现
①最优子结构性质
0-1背包问题具有最优子结构性质。
设(y1,y2…yn)是所给0-1背包问题的一个最优解,则(y2,y3…yn)是下面相应子问题的一个最优解:
因若不然,设(z2,z3…zn)是上述问题的一个最优解,而(y2,y3…yn)不是它的最优解,由此可见
且
c。
因此
c
这说明(y1,z2…zn)是所给0-1背包问题的一个更优解,从而(y1,y2…yn)不是所给0-1背包问题的最优解。
此为矛盾[1]。
②递归关系
设所给0-1背包问题的子问题
的最优值为m(i,j),即m(i,j)是背包容量为j,可选择物品为i,i+1,……,n时0-1背包问题的最优值。
由0-1背包问题的最优子结构性质,可以建立计算m(i,j)的递归式如下:
3、算法设计如下:
#include
#include
usingnamespacestd;
constintMAX=1000;
intw[MAX],v[MAX],best[MAX];
intV[MAX][MAX];//最大价值矩阵
intW,n;//W为背包的最大载重量,n为物品的数量
//求最大值函数
intmax(intx,inty)
{
returnx>=y?
x:
y;
}
//求最小值函数
intmin(intx,inty)
{
returnx>=y?
y:
x;
}
voidKnaspack()
{
intMax=min(w[n]-1,W);
for(intj=1;j<=Max;j++)
V[n][j]=0;
for(j=w[n];j<=W;j++)
V[n][j]=v[n];
for(inti=n-1;i>1;i--)
{
Max=min(w[i]-1,W);
for(j=1;j<=Max;j++)
V[i][j]=V[i+1][j];
for(j=w[i];j<=W;j++)
V[i][j]=max(V[i+1][j],V[i+1][j-w[i]]+v[i]);
}
V[1][W]=V[2][W];//先假设第一个物品不放入
if(W>w[1])
V[1][W]=max(V[1][W],V[2][W-w[1]]+v[1]);
}
//生成向量数组,决定某一个物品是否应该放入背包
voidTraceback()
{
for(inti=1;i{
if(V[i][W]==V[i+1][W])//如果当前行的最优值与下一行的最优值相等,则表明该物品不能放入。
best[i]=0;
else//否则可以放入
{
best[i]=1;
W-=w[i];
}
}
best[n]=(V[n][W])?
1:
0;
}
voidmain()
{
cout<<"输入商品数量n和背包容量W:
";
cin>>n>>W;
cout<<"输入每件商品的重量w:
"<for(inti=1;i<=n;i++)
cin>>w[i];
memset(V,0,sizeof(V));
cout<<"输入每件商品的价值v:
"<for(i=1;i<=n;i++)
cin>>v[i];
Knaspack();//构造矩阵
Traceback();//求出解的向量数组
inttotalW=0;
inttotalV=0;
//显示可以放入的物品
cout<<"所选择的商品如下:
"<cout<<"序号i:
重量w:
价格v:
"<for(i=1;i<=n;i++)
{
if(best[i]==1)
{
totalW+=w[i];
totalV+=v[i];
cout<:
left)<}
}
cout<<"放入的物品重量总和是:
"<"<}
4、计算复杂性分析
利用动态规划求解0-1背包问题的复杂度为0(min{nc,2n}。
动态规划主要是求解最优决策序列,当最优决策序列中包含最优决策子序列时,可建立动态规划递归方程,它可以帮助高效地解决问题[8]。
5、动态规划运行结果如下图所示:
方案三:
回溯法
1、回溯法的基本原理与分析
回溯是一种系统地搜索问题解答的方法。
为了实现回溯,首先需要为问题定义一个解空间,这个解空间必须至少包含问题的一个解(可能是最优的)。
回溯法需要为问题定义一个解空间,这个解空间必须至少包含问题的一个解(可能是最优的)。
使用递归回溯法解决背包问题的优点在于它算法思想简单,而且它能完全遍历搜索空间,肯定能找到问题的最优解奉但是由于此问题解的总组合数有
个,因此随着物件数n的增大,其解的空间将以
n
级增长,当n大到一定程度上,用此算法解决背包问题将是不现实的。
下一步是组织解空间以便它能被容易地搜索。
典型的组织方法是图或树。
一旦定义了解空间的组织方法,这个空间即可按照深度优先的方法从开始结点进行搜索,利用限界函数避免移动到不可能产生解的子空间。
2、0-1背包问题的实现
回溯法是一种系统地搜索问题解答的方法。
为了实现回溯,首先需要为问题定义一个解空间,这个解空间必须至少包含问题的一个解(可能是最优的)。
一旦定义了解空间的组织方要选择一个对象的子集,将它们装人背包,以便获得的收益最大,则解空间应组织成子集树的形状。
首先形成一个递归算法,去找到可获得的最大收益。
然后,对该算法加以改进,形成代码。
改进后的代码可找到获得最大收益时包含在背包中的对象的集合。
左子树表示一个可行的结点,无论何时都要移动到它,当右子树可能含有比当前最优解还优的解时,移动到它。
一种决定是否要移动到右子树的简单方法是r为还未遍历的对象的收益之和,将r加到cp(当前节点所获收益)之上,若(r+cp)
bestp(目前最优解的收益),则不需搜索右子树。
一种更有效的方法是按收益密度vi/wi对剩余对象排序,将对象按密度递减的顺序去填充背包的剩余容量,当遇到第一个不能全部放人背包的对象时,就使用它的一部分。
3、算法设计如下:
#include
usingnamespacestd;
classKnap
{
friendintKnapsack(intp[],intw[],intc,intn);
public:
voidprint()
{
for(intm=1;m<=n;m++)
{
cout<}
cout<};
private:
intBound(inti);
voidBacktrack(inti);
intc;//背包容量
intn;//物品数
int*w;//物品重量数组
int*p;//物品价值数组
intcw;//当前重量
intcp;//当前价值
intbestp;//当前最优值
int*bestx;//当前最优解
int*x;//当前解
};
intKnap:
:
Bound(inti)
{
//计算上界
intcleft=c-cw;//剩余容量
intb=cp;
//以物品单位重量价值递减序装入物品
while(i<=n&&w[i]<=cleft)
{
cleft-=w[i];
b+=p[i];
i++;
}
//装满背包
if(i<=n)
b+=p[i]/w[i]*cleft;
returnb;
}
voidKnap:
:
Backtrack(inti)
{
if(i>n)
{
if(bestp{
for(intj=1;j<=n;j++)
bestx[j]=x[j];
bestp=cp;
}
return;
}
if(cw+w[i]<=c)//搜索左子树
{
x[i]=1;
cw+=w[i];
cp+=p[i];
Backtrack(i+1);
cw-=w[i];
cp-=p[i];
}
if(Bound(i+1)>bestp)//搜索右子树
{
x[i]=0;
Backtrack(i+1);
}
}
classObject
{
friendintKnapsack(intp[],intw[],intc,intn);
public:
intoperator<=(Objecta)const
{
return(d>=a.d);
}
private:
intID;
floatd;
};
intKnapsack(intp[],intw[],intc,intn)
{
//为Knap:
:
Backtrack初始化
intW=0;
intP=0;
inti=1;
Object*Q=newObject[n];
for(i=1;i<=n;i++)
{
Q[i-1].ID=i;
Q[i-1].d=1.0*p[i]/w[i];
P+=p[i];
W+=w[i];
}
if(W<=c)
returnP;//装入所有物品
//依物品单位重量排序
floatf;
for(i=0;ifor(intj=i;j{
if(Q[i].d{
f=Q[i].d;
Q[i].d=Q[j].d;
Q[j].d=f;
}
}
KnapK;
K.p=newint[n+1];
K.w=newint[n+1];
K.x=newint[n+1];
K.bestx=newint[n+1];
K.x[0]=0;
K.bestx[0]=0;
for(i=1;i<=n;i++)
{
K.p[i]=p[Q[i-1].ID];
K.w[i]=w[Q[i-1].ID];
}
K.cp=0;
K.cw=0;
K.c=c;
K.n=n;
K.bestp=0;
//回溯搜索
K.Backtrack
(1);
K.print();
delete[]Q;
delete[]K.w;
delete[]K.p;
returnK.bestp;
}
voidmain()
{
int*p;
int*w;
intc=0;
intn=0;
inti=0;
chark;
while(k)
{
cout<<"请输入背包容量(c):
"<cin>>c;
cout<<"请输入物品的个数(n):
"<cin>>n;
p=newint[n+1];
w=newint[n+1];
p[0]=0;
w[0]=0;
cout<<"请输入物品的价值(p):
"<for(i=1;i<=n;i++)
cin>>p[i];
cout<<"请输入物品的重量(w):
"<for(i=1;i<=n;i++)
cin>>w[i];
cout<<"最优解为(bestx):
"<cout<<"最优值为(bestp):
"<cout<cout<<"[s]重新开始"<cout<<"[q]退出"<cin>>k;
}
}
4、运行结果如下图所示:
方案四:
分枝-限界法
1、分枝-限界法的基本原理与分析
分枝限界发是另一种系统地搜索解空间的方法,它与回溯法的主要区别在于对E-结点(expansionnode)的扩充方式。
每个活结点有且仅有一次会变成E-结点。
当一个结点变为E-结点时,则生成从该结点移动一步即可到达的所有新结点。
在生成的结点中,抛弃那些不可能导出(最优)可行解的结点,其余结点加人活结点表,然后从表中选择一个结点作为下一个E结点。
从活结点表中取出所选择的结点并进行扩充,直到找到解或活动表为空,扩充才结束。
2、0-1背包问题的实现
0-1背包问题的最大收益分枝定界算法可以使用定界函数来计算活结点的收益上限upprofit,使得以活结点为根的子树中的任一结点的收益值都不可能超过upprofit,活结点的最大堆使用upprofit作为关键值域。
在子集树中执行最大收益分枝定界搜索的函数首先初始化活结点的最大堆,并使用一个数组bestx来记录最优解。
由于需要不断地利用收益密度来排序,物品的索引值会随之变化,因此必须将函数所生成的结果映射回初始时的物品索引。
函数中的循环首先检验E-结点左孩子的可行性,如它是可行的,则将它加入子集树及活结点队列(即最大堆),仅当结点右子树的定界值指明可能找到一个最优解时才将右孩子加入子集树和队列中。
3、算法设计:
#include
classKnap;
classObject;
classObject
{
friendintKnapsack(int*,int*,int,int,int*);
public:
intoperator<=(Objecta)const
{
return(d>=a.d);
}
private:
intID;
floatd;//单位重量价值
};
classbbnode
{
friendKnap;
friendintKanpsack(int*,int*,int,int,int*);
private:
bbnode*parent;//指向父节点的指针
boolLChild;//左儿子结点标志
};
classHeapNode
{
friendKnap;
public:
operatorint()const
{returnuprofit;}
voidInsert(HeapNodeN);
voidDeleteMax(HeapNodeN);
private:
intuprofit,//结点的价值上界
profit;//结点所对应的价值
intweight;//结点所对应的重量
intlevel;//活结点在子集树中所处的层序号
bbnode*ptr;//指向活结点在子集树中相应结点的指针
};
voidHeapNode:
:
Insert(HeapNodeN)
{
}
voidHeapNode:
:
DeleteMax(HeapNodeN)
{
}
classKnap
{
friendintKnapsack(int*,int*,int,int,int*);
public:
intMaxKnapsack();
private:
HeapNode*H;
intMaxBoundary(inti);
voidAddLiveNode(intup,intcp,intcw,boolch,intlevel);
bbnode*E;//指向扩展结点的指针
intc;//背包容量
intn;//物品总数
int*w;//物品重量数组
int*p;//物品价值数组
intcw;//当前背包重量
intcp;//当前背包价值
int*bestx;//最优解的记录数组
};
//计算所相应的价值的上界
intKnap:
:
MaxBoundary(inti)
{
intcleft=c-cw;//剩余容量
intb=cp;//价值上限
//以物品单位重量价值递减序装填剩余容量
while(i<=n&&w[i]<=cleft)
{
cleft-=w[i];
b+=p[i];
i++;
}
//将背包的剩余容量装满
if(i<=n)
b+=p[i]/w[i]*cleft;
returnb;
}
//将一个新的活结点插入到子集树和优先队列中
voidKnap:
:
AddLiv