问题描述
有n个重量分别为{w1,w2,…,wn}的物品,它们的价值分别为{v1,v2,…,vn},给定一个容量为W的背包。
设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且重量和为W具有最大的价值。
假设一个0/1背包问题是,n=3,重量为w=(16,15,15),价值为v=(45,25,25),背包限重为W=30,解向量为x=(x1,x2,x3)。
采用队列式分枝限界法求解
采用STL的queue< NodeType >容器qu作为队列,队列中的结点类型声明如下:
struct NodeType //队列中的结点类型
{ int no; //结点编号,从1开始
int i; //当前结点在搜索空间中的层次
int w; //当前结点的总重量
int v; //当前结点的总价值
int x[MAXN]; //当前结点包含的解向量
double ub; //上界
};
现在设计限界函数,为了简便,设根结点为第0层,然后各层依次递增,显然i=n时表示是叶子结点层。
由于该问题是求装入背包的最大价值,属求最大值问题,采用上界设计方式。
对于第i层的某个结点e,用e.w表示结点e时已装入的总重量,用e.v表示已装入的总价值:
如果所有剩余的物品都能装入背包,那么价值的上界e.ub=e.v+ (v[i+1]+…+v[n])
如果所有剩余的物品不能全部装入背包,那么价值的上界e.ub=e.v+ (v[i+1]+…+v[k])+(物品k+1装入的部分重量)×物品k+1的单位价值
代码
int n=3,W=30;
int w[]={0,16,15,15};
int v[]={0,45,25,25};
int maxv=-9999;
int bestx[MAXN];
int total=1;
struct NodeType
{
int no;//节点编号,从1开始
int i;//当前节点在搜索空间中的层次
int w;//当前节点的总重量
int v;//当前节点的总价值
int x[MAXN];//当前节点包含的解向量
double ub;//上界
};
void bound(NodeType &e)//计算分枝节点e的上界
{
int i=e.i+1;//考虑余下的物品
int sumw=e.w;
double sumv=e.v;
while((sumw+w[i]<=W)&&i<=n)
{
sumw+=w[i];
sumv+=v[i];
i++;
}
if(i<=n)//物品只能部分装入
e.ub=sumv+(W-sumw)*v[i]/w[i];
else//可以全部装入
e.ub=sumv;
}
void EnQueue(NodeType e,queue<NodeType> &qu)
{
if(e.i==n)//到达叶子节点
{
if(e.v>maxv)
{
maxv=e.v;
for(int j=1;j<=n;j++)
bestx[j]=e.x[j];
}
}
else
{
qu.push(e);
}
}
void bfs()
{
int j;
NodeType e,e1,e2;
queue<NodeType> qu;
//根节点初始化
e.i=0;
e.w=0;
e.v=0;
e.no=total++;
for(j=1;j<=n;j++)
e.x[j]=0;
bound(e);
qu.push(e);
while (!qu.empty())
{
e=qu.front();
qu.pop();
if(e.w+w[e.i+1]<=W)//检查左孩子节点
{
e1.no=total++;
e1.i=e.i+1;
e1.w=e.w+w[e1.i];
e1.v=e.v+v[e1.i];
for(j=1;j<=n;j++)
e1.x[j]=e.x[j];
e1.x[e1.i]=1;//选择物品
bound(e1);
EnQueue(e1,qu);
}
e2.no=total++;
e2.i=e.i+1;
e2.w=e.w;
e2.v=e.v;
for(j=1;j<=n;j++)
e2.x[j]=e.x[j];
e2.x[e2.i]=0;//不选择物品
bound(e2);
if(e2.ub>maxv)//右剪枝
EnQueue(e2,qu);
}
}
采用优先队列式分枝限界法求解
采用优先队列式分枝限界法求解就是将一般的队列改为优先队列,但必须设计限界函数,因为优先级是以限界函数值为基础的。
限界函数的设计方法与前面的相同。这里用大根堆表示活结点表,取优先级为活结点所获得的价值。
struct NodeType //队列中的结点类型
{ int no; //结点编号
int i; //当前结点在搜索空间中的层次
int w; //当前结点的总重量
int v; //当前结点的总价值
int x[MAXN]; //当前结点包含的解向量
double ub; //上界
bool operator<(const NodeType &s) const //重载<关系函数
{
return ub<s.ub; //ub越大越优先出队
}
};
代码
int n=3,W=30;
int w[]={0,16,15,15};
int v[]={0,45,25,25};
int maxv=-9999;
int bestx[MAXN];
int total=1;
struct NodeType
{
int no;//节点编号,从1开始
int i;//当前节点在搜索空间中的层次
int w;//当前节点的总重量
int v;//当前节点的总价值
int x[MAXN];//当前节点包含的解向量
double ub;//上界
bool operator<(const NodeType &s) const
{
return ub<s.ub;//ub越大越优先出队
}
};
void bound(NodeType &e)//计算分枝节点e的上界
{
int i=e.i+1;//考虑余下的物品
int sumw=e.w;
double sumv=e.v;
while((sumw+w[i]<=W)&&i<=n)
{
sumw+=w[i];
sumv+=v[i];
i++;
}
if(i<=n)//物品只能部分装入
e.ub=sumv+(W-sumw)*v[i]/w[i];
else//可以全部装入
e.ub=sumv;
}
void EnQueue(NodeType e,queue<NodeType> &qu)
{
if(e.i==n)//到达叶子节点
{
if(e.v>maxv)
{
maxv=e.v;
for(int j=1;j<=n;j++)
bestx[j]=e.x[j];
}
}
else
{
qu.push(e);
}
}
void bfs()
{
int j;
NodeType e,e1,e2;
priority_queue<NodeType> qu;
//根节点初始化
e.i=0;
e.w=0;
e.v=0;
e.no=total++;
for(j=1;j<=n;j++)
e.x[j]=0;
bound(e);
qu.push(e);
while (!qu.empty())
{
e=qu.front();
qu.pop();
if(e.w+w[e.i+1]<=W)//检查左孩子节点
{
e1.no=total++;
e1.i=e.i+1;
e1.w=e.w+w[e1.i];
e1.v=e.v+v[e1.i];
for(j=1;j<=n;j++)
e1.x[j]=e.x[j];
e1.x[e1.i]=1;//选择物品
bound(e1);
EnQueue(e1,qu);
}
e2.no=total++;
e2.i=e.i+1;
e2.w=e.w;
e2.v=e.v;
for(j=1;j<=n;j++)
e2.x[j]=e.x[j];
e2.x[e2.i]=0;//不选择物品
bound(e2);
if(e2.ub>maxv)//右剪枝
EnQueue(e2,qu);
}
}
算法分析
无论采用队列式分枝限界法还是优先队列式分枝限界法求解0/1背包问题,最坏情况下要搜索整个解空间树,所以最坏时间和空间复杂度均为O(2n),其中n为物品个数。