01背包问题的各种算法求解.docx
《01背包问题的各种算法求解.docx》由会员分享,可在线阅读,更多相关《01背包问题的各种算法求解.docx(16页珍藏版)》请在冰豆网上搜索。
![01背包问题的各种算法求解.docx](https://file1.bdocx.com/fileroot1/2023-1/6/dd838233-fbae-4621-a182-b2542feacd96/dd838233-fbae-4621-a182-b2542feacd961.gif)
01背包问题的各种算法求解
一.动态规划求解0-1背包问题
/************************************************************************/
/*0-1背包问题:
/*给定n种物品和一个背包
/*物品i的重量为wi,其价值为vi
/*背包的容量为c
/*应如何选择装入背包的物品,使得装入背包中的物品
/*的总价值最大?
/*注:
在选择装入背包的物品时,对物品i只有两种选择,
/*即装入或不装入背包。
不能将物品i装入多次,也
/*不能只装入部分的物品i。
/*
/*1.0-1背包问题的形式化描述:
/*给定c>0,wi>0,vi>0,0<=i<=n,要求找到一个n元的
/*0-1向量(x1,x2,...,xn),使得:
/*maxsum_{i=1ton}(vi*xi),且满足如下约束:
/*
(1)sum_{i=1ton}(wi*xi)<=c
/*
(2)xi∈{0,1},1<=i<=n
/*
/*2.0-1背包问题的求解
/*0-1背包问题具有最优子结构性质和子问题重叠性质,适于
/*采用动态规划方法求解
/*
/*2.1最优子结构性质
/*设(y1,y2,...,yn)是给定0-1背包问题的一个最优解,则必有
/*结论,(y2,y3,...,yn)是如下子问题的一个最优解:
/*maxsum_{i=2ton}(vi*xi)
/*
(1)sum_{i=2ton}(wi*xi)<=c-w1*y1
/*
(2)xi∈{0,1},2<=i<=n
/*因为如若不然,则该子问题存在一个最优解(z2,z3,...,zn),
/*而(y2,y3,...,yn)不是其最优解。
那么有:
/*sum_{i=2ton}(vi*zi)>sum_{i=2ton}(vi*yi)
/*且,w1*y1+sum_{i=2ton}(wi*zi)<=c
/*进一步有:
/*v1*y1+sum_{i=2ton}(vi*zi)>sum_{i=1ton}(vi*yi)
/*w1*y1+sum_{i=2ton}(wi*zi)<=c
/*这说明:
(y1,z2,z3,...zn)是所给0-1背包问题的更优解,那么
/*说明(y1,y2,...,yn)不是问题的最优解,与前提矛盾,所以最优
/*子结构性质成立。
/*
/*2.2子问题重叠性质
/*设所给0-1背包问题的子问题P(i,j)为:
/*maxsum_{k=iton}(vk*xk)
/*
(1)sum_{k=iton}(wk*xk)<=j
/*
(2)xk∈{0,1},i<=k<=n
/*问题P(i,j)是背包容量为j、可选物品为i,i+1,...,n时的子问题
/*设m(i,j)是子问题P(i,j)的最优值,即最大总价值。
则根据最优
/*子结构性质,可以建立m(i,j)的递归式:
/*a.递归初始m(n,j)
/*//背包容量为j、可选物品只有n,若背包容量j大于物品n的
/*//重量,则直接装入;否则无法装入。
/*m(n,j)=vn,j>=wn
/*m(n,j)=0,0<=j/*b.递归式m(i,j)
/*//背包容量为j、可选物品为i,i+1,...,n
/*//如果背包容量j/*m(i,j)=m(i+1,j),0<=j/*//如果j>=wi,则在不装物品i和装入物品i之间做出选择
/*不装物品i的最优值:
m(i+1,j)
/*装入物品i的最优值:
m(i+1,j-wi)+vi
/*所以:
/*m(i,j)=max{m(i+1,j),m(i+1,j-wi)+vi},j>=wi
/*
/************************************************************************/
#definemax(a,b)(((a)>(b))?
(a):
(b))
#definemin(a,b)(((a)<(b))?
(a):
(b))
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];
}
//i从2到n-1,分别对j>=wi和0<=jfor(inti=n-1;i>1;i--){
jMax=min(w[i]-1,c);
for(intj=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)
{
for(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;
}
intmain(intargc,char*argv[])
{
intn=5;
intw[6]={-1,2,2,6,5,4};
intv[6]={-1,6,3,5,4,6};
intc=10;
int**ppm=newint*[n+1];
for(inti=0;ippm[i]=newint[c+1];
}
intx[6];
Knapsack(v,w,c,n,ppm);
TraceBack(ppm,w,c,n,x);
return0;}
二.贪心算法求解0-1背包问题
1.贪心法的基本思路:
——从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。
当达到某算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1).不能保证求得的最后解是最佳的;
2).不能用来求最大或最小解问题;
3).只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while能朝给定总目标前进一步do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
2.例题分析
1).[背包问题]有一个背包,背包容量是M=150。
有7个物品,物品可以分割成任意大小。
要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品ABCDEFG
重量35306050401025
价值10403050354030
分析:
目标函数:
∑pi最大
约束条件是装入的物品总重量不超过背包容量:
∑wi<=M(M=150)
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占空间最小的物品装入是否能得到最优解?
(3)每次选取单位容量价值最大的物品,成为解本题的策略。
<程序代码:
>(环境:
c++)
#include
#definemax100//最多物品数
voidsort(intn,floata[max],floatb[max])//按价值密度排序
{
intj,h,k;
floatt1,t2,t3,c[max];
for(k=1;k<=n;k++)
c[k]=a[k]/b[k];
for(h=1;hfor(j=1;j<=n-h;j++)
if(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=1;i<=n;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];
}
cout<cout<<"背包的总重量为:
"<cout<<"背包的总价值为:
"<}
三.回溯算法求解0-1背包问题
1.0-l背包问题是子集选取问题。
一般情况下,0-1背包问题是NP难题。
0-1背包问题的解空间可用子集树表示。
解0-1背包问题的回溯法与装载问题的回溯法类似。
在搜索解空间树时,只要其左孩子结点是一个可行结点,搜索就进入其左子树。
当右子树有可能包含最优解时才进入右子树搜索。
否则将右子树剪去。
设r是当前剩余物品价值总和;cp是当前价值;bestp是当前最优价值。
当cp+r≤bestp时,可剪去右子树。
计算右子树中解的上界的更好方法是将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下时,再装入该物品的一部分而装满背包。
由此得到的价值是右子树中解的上界。
2.解决办法思路:
为了便于计算上界,可先将物品依其单位重量价值从大到小排序,此后只要顺序考察各物品即可。
在实现时,由bound计算当前结点处的上界。
在搜索解空间树时,只要其左孩子节点是一个可行结点,搜索就进入左子树,在右子树中有可能包含最优解时才进入右子树搜索。
否则将右子树剪去。
回溯法是一个既带有系统性又带有跳跃性的搜索算法。
它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。
算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。
如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。
否则,进入该子树,继续按深度优先的策略进行搜索。
回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。
而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。
这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
2.算法框架:
a.问题的解空间:
应用回溯法解问题时,首先应明确定义问题的解空间。
问题的解空间应到少包含问题的一个(最优)解。
b.回溯法的基本思想:
回溯法就从根结点出发,以深度优先的方式搜索整个解空间。
根结点成为一个活结点,同时也成为当前的扩展结点。
在当前的扩展结点处,搜索向纵深方向移至一个新结点。
这个新结点就成为一个新的活结点,并成为当前扩展结点。
如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。
换句话说,这个结点不再是一个活结点。
此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。
回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。
3.运用回溯法解题通常包含以下三个步骤:
a.针对所给问题,定义问题的解空间;
b.确定易于搜索的解空间结构;
c.以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索;
#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;
cout<<"0-1背包问题——回溯法"<cout<<"byzbqplayer"<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;
}
四.分支限界法求解0-1背包问题
1.问题描述:
已知有N个物品和一个可以容纳M重量的背包,每种物品I的重量为WEIGHT,一个只能全放入或者不放入,求解如何放入物品,可以使背包里的物品的总效益最大。
2.设计思想与分析:
对物品的选取与否构成一棵解树,左子树表示不装入,右表示装入,通过检索问题的解树得出最优解,并用结点上界杀死不符合要求的结点。
#include
structgood
{
intweight;
intbenefit;
intflag;//是否可以装入标记
};
intnumber=0;//物品数量
intupbound=0;
intcurp=0,curw=0;//当前效益值与重量
intmaxweight=0;
good*bag=NULL;
voidInit_good()
{
bag=newgood[number];
for(inti=0;i{
cout<<"请输入第件"<
";
cin>>bag[i].weight;
cout<<"请输入第件"<
";
cin>>bag[i].benefit;
bag[i].flag=0;//初始标志为不装入背包
cout<}
}
intgetbound(intnum,int*bound_u)//返回本结点的c限界和u限界
{
for(intw=curw,p=curp;num{
w=w+bag[num].weight;
p=w+bag[num].benefit;
}
*bound_u=p+bag[num].benefit;
return(p+bag[num].benefit*((maxweight-w)/bag[num].weight));
}
voidLCbag()
{
intbound_u=0,bound_c=0;//当前结点的c限界和u限界
for(inti=0;i{
if((bound_c=getbound(i+1,&bound_u))>upbound)//遍历左子树
upbound=bound_u;//更改已有u限界,不更改标志
if(getbound(i,&bound_u)>bound_c)//遍历右子树
//若装入,判断右子树的c限界是否大于左子树根的c限界,是则装入
{
upbound=bound_u;//更改已有u限界
curp=curp+bag[i].benefit;
curw=curw+bag[i].weight;//从已有重量和效益加上新物品
bag[i].flag=1;//标记为装入
}
}
}
voidDisplay()
{
cout<<"可以放入背包的物品的编号为:
";
for(inti=0;i<