0-1背包问题-队列分支限界法
问题描述:
给定n种物品和一个背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?对于给定的n种物品的重量和价值,以及背包的容量,计算可装入背包的最大价值。
算法设计过程:
考虑使用队列分支限界法来解决来解决0-1背包问题,对于每一个物品,我们都有选或者不选两种情况,因此该问题的解空间为一颗子集树。
要采用队列来求解该问题,对于队列中的每一个节点,都要维护当前节点所选择的物品集合,同时维护当前节点的总价值和容量,为剪去不可行解和优化搜索过程做准备。
对于该问题,我们在使用队列分支限界的搜索过程,在处理当前节点时,可以考虑对左右儿子进行约束和限界:
对于该节点的左儿子,我们要考虑其是否满足约束,具体的,我们要考虑当前背包剩余空间能否装下这个物品,也就是当前的总重加上该物品的总量要小于背包总容量时我们才考虑进入左儿子节点。否则左儿子就是一个不可行解,直接跳过即可。
对于该节点的右儿子,我们考虑设计一个限界函数bound来限制进入右儿子的条件,在搜索过程我们维护一个当前的最优解,我们要通过计算进入右子树后有没有可能得到更优的解,只有右儿子包含最优解时我们才选择进入。具体的,设cv是当前的价值,我们考虑将该物品跳过后的最优价值为r,对于当前的最优解bstv,若是\cv+r<=bstv时,我们便考虑剪去右子树,为了更好的实现,我们需要在搜索之前对物品按单位重量价值进行排序,然后依次装入物品,直到装不下时,再装入该物品的一部分而装满背包,由此得到右子树的上界bound。
对于该问题我们的具体算法流程:
1.对所有物品按单位重量价值从大到小进行排序。
2.初始化队列,加入初始的节点,这里以第一个物品作为根节点加入。
3.当队列不为空时,每次从队列中取出一个节点的进行扩展。
4.如果左儿子是可行节点,即满足约束条件时,如果该结点的价值大于最优解,则更新最优解,同时将该节点加入队列。
5.对于扩展节点的右儿子,当其满足限界函数时才将其加队列中。
6.当队列为空时,说明搜索过程已经结束,直接返回最优解即可。
流程图:
代码:
#include <bits/stdc++.h>
using namespace std;
struct Obj{ // 物品
int id;
int w;
int val;
bool operator<(Obj &obj){
return val * obj.w > obj.val * w;
}
};
struct Node {
int dep; // 深度,第几层就是处理第几个物品
int cv; /// 当前价值
int cw; // 当前容量
vector<int>x; // 解向量
Node(int dep,int cv, int cw, vector<int>x):dep(dep), cv(cv), cw(cw), x(x) {}
friend ostream& operator<<(ostream& os, const Node& p){
cout << "dep:" << p.dep << " cv:" << p.cv << " cw:" << p.cw;
cout << " x:";
for(int i:p.x)cout << i << ' ';
return os;
};
};
struct Whopxx{
int n; // 物品数量
vector<Obj>vt; // 物品
int sum; // 背包大小
vector<int>bstx; // 最优解
int bstv;
Whopxx(int n,int sum, vector<int>w, vector<int>p):n(n), sum(sum) {
bstx.resize(n + 1);
bstv = 0;
vt.resize(n + 1);
for(int i = 1; i <= n; i++){
vt[i] = {i, w[i], p[i]};
}
sort(vt.begin() + 1, vt.end()); // 按单价从大到小排序
}
void work(){
queue<Node> q;
q.push(Node(1, 0, 0, {})); // 第一个物品开始做选择
while(q.size()){
Node node = q.front(); q.pop();
cout << node << " bound:" << Bound(node) << " bstv:" << bstv << endl;
int i = node.dep;
if(i > n || node.cw == sum){ // 到达叶子结点或背包已满
if(node.cv > bstv) update(node); // 更新最大值
continue;
}
auto [id, w, val] = vt[i];
if(node.cw + w <= sum){ // 左儿子满足约束
Node lnode = add(node, vt[i]);
q.push(lnode);
if(lnode.cv > bstv) update(lnode);
}
if(Bound(node) > 1.0 * bstv){ // 右儿子满足限界
Node rnode = uadd(node);
q.push(rnode);
}
}
}
double Bound(Node node){ // 限界函数
int rw = sum - node.cw; // 剩余容量
int i = node.dep + 1;
double b = node.cv;
while(i <= n && vt[i].w <= rw){
rw -= vt[i].w;
b += vt[i].val;
i++;
}
if(i <= n){
b += vt[i].val * 1.0 / vt[i].w * rw;
}
return b;
}
Node add(Node node, Obj obj){
node.cv += obj.val;
node.cw += obj.w;
node.x.push_back(obj.id);
node.dep += 1;
return node;
}
Node uadd(Node node){
node.dep += 1;
return node;
}
void update(Node node){
bstx = node.x;
bstv = node.cv;
}
};
int main(){
freopen("input.txt","r", stdin);
freopen("output.txt", "w", stdout);
int n, c; // 物品数量,背包容量
cin >> n >> c;
vector<int>w(n + 1);
vector<int>p(n + 1);
for(int i = 1; i <= n; i++) cin >> p[i];
for(int i = 1; i <= n; i++) cin >> w[i];
Whopxx wx(n, c, w, p);
wx.work();
vector<int>ans(n + 1);
for(int i:wx.bstx) ans[i] = 1;
cout << wx.bstv << endl;
for(int i = 1; i <= n; i++){
cout << ans[i] << ' ';
}
fclose(stdin);
fclose(stdout);
return 0;
}
/*
4 7
9 10 7 4
3 5 2 1
3 30
45 25 25
16 15 15
4 15
10 10 12 18
2 4 6 9
5 10
2 2 6 5 4
6 3 5 4 6
4 10
40 42 25 12
4 7 5 3
*/
实验测试结果及分析:
测试数据:
input.txt
通过运行程序得到:
output.txt
在该输出结果中,cv是当前节点的物品价值总和,cw是当前总重,bound是进入右子树后的最大值,bstv是进行到当前节点的全局最优解得值,形象的可以用如下图来表示该搜索过程:
最终我们得到最优解即选择物品1和物品3装入背包,得到的最优总价值为65。
复杂度分析:限界函数的时间复杂度为O(n),在最坏的情况下有2^n个右儿子结点需要计算上界,所以计算需要的最坏情况下的时间复杂度为O(n2 ^n)。