优先队列分支限界法求01背包——手写堆140行——priority_queue 100行

算法设计与分析的作业,老实说,这个方法挺强的,但是网上的参考基本都是指针+课设,而且解释不清晰,真心难参考····,这里我尽可能简单的重述思路和代码。
01背包是个老问题,n个物品,每个物品装或者不装,每个物品都有自己的价值。问在背包容量范围内能装入的物品的最大总价值。
正常做法是DP。当然,dfs,bfs也能写,问题就是太慢了,时间复杂度O(2^n),这谁顶得住啊。
开始正题

与其他方法的比较
考虑二叉树解决01背包,取和不取两种状态分别用左孩子和右孩子表示,所有状况表示构成一棵完全二叉树。
普通bfs就是逐层处理,每一层代表一个物品,然后比较所有2^n种情况(不考虑剪枝)的最终价值。
优先队列分支限界法本质还是bfs,和普通的bfs不同的是,我们对于每一个点求一个bound,根据bound的大小来确定bfs的顺序。
也就是,普通的bfs是逐层把节点加入queue,优先队列分支限界法则是根据bound的大小,先把bound比较大的节点加入queue。
虽然最坏情况下复杂度也是o(2^n),但是平均时间复杂度非常好,对于随机数据,这个写法和普通的dp写法时间不相上下。(洛谷p1048,此写法38ms,dp写法31ms).当然,这个跟数据有关。
根本原因就是优先队列分支限界法求01背包和普通bfs相比,bfs要搜索所有情况,但是这个方法处理到的第一个叶子节点,必然就是答案,可以直接跳过其他所有情况。

具体求法
bound是什么?答:一个上界 具体来说就是当前节点之后可能达到的最大值
只要能够确定bound,接下来就是把bfs的queue换成priority_queue,这个做法剩下的东西就和普通的bfs没有任何区别。

我们提到了上界是当前节点之后可能达到的最大值,求法也简单,自然就是:
已经有的价值 + 所有没取过的物品能得到的最大价值
假设当前节点为cur,cur.v就是当前节点含有的物品价值,cur所在层和更深的层所代表的物品,就是我们求上界要考虑的对象。
注意:上界是可能达到的最大值,也就是说我们最后的结果可以不达到上界。那就简单了,按照分数背包处理,即一个物品我可以只取走一部分,那我取物品的顺序就是单位价值最大的优先取,如果背包容量不够了,就尽可能取走。
那cur.bound = cur.v + 尽可能取的按单位价值排序后的物品的价值

double cal_bound(int x,int v){//当前物品编号和剩余体积
	double res=0;// 尽可能取的按单位价值排序后的物品的价值
	for(int i=x;i<=n;i++){//剩下的所有物品 能装就装
		if(a[i].w<=v){
			v-=a[i].w;
			res+=a[i].v;
		}
		else{//装不下  拆成一部分装
			res+=a[i].f*v; v=0;
			break;
		}
	}
	return res;
}

那么,上界处理完了。
说说bfs的过程。
我的做法是对于一棵二叉树,高度从上到下依次为0~n,高度0不表示物品,高度1到n分别表示物品1到n,bfs开始时,手动插入高度0的节点first, 然后while(!empty()),对于每一个节点,分别求其左孩子节点(取)和右孩子(不取)然后加入优先队列,优先队列自动排序即可。
然后就是一个层数为n+1,记录结果,跳出,结束。

整理思路

  1. 读入数据,所有物品按照单位价值排序
  2. bfs,每个节点求bound,子节点加入队列后继续搜索。
  3. 是否搜索到了最后
  4. 输出

比较麻烦的地方就是要记录的东西太多了:
每个物品,记录原序号id, 重量w, 价值v, 单位价值v/w(排序用),上界bound,排序后的序号level(对应树高),已装入物品path(vector一波)
然后是结构体的比较通过重载<和>实现了,省的每次用内部变量麻烦。
最后就是手写堆真容易出锅·····建议好好研究手写堆,我大数据WA了就是因为堆模板有问题,一度自闭,不知道哪里错了。。。

贴两个版本

代码:(手写堆实现版本)

#include 
#include 
#include 
using namespace std;
//分支限界法求01背包
const int N=1005;
struct item{
	int w,v,id,level;//已装入物品的重量 价值  id表示当前物品的原编号 level表示现编号(高度)
	double f,bound;// f 单位价值  bound上界
	vector<int>path;//记录当前节点的到达方式(选取的物品)
	item(){//构造函数
		w=0; v=0; id=0;level=0; f=0; bound=0;
		path.clear();
	}
}a[N];
int total,n,res=0;//背包总容量  物品总数量 题目求的最大价值
vector<int> respath;//记录物品选取情况
bool cmp1(item A,item B){
	return A.f>B.f;//按照单位价值排序用
}
bool operator <(const item &A, const item &B){//重载运算符 让item类型可以直接比较大小
		return A.bound<B.bound;
}
bool operator >(const item &A, const item &B){
		return A.bound>B.bound;
}
	//手写大根堆
item q[N*10];int cnt=0;//队列开大,队列存点可能很多
void push(item x){//存入数字
	q[++cnt]=x;
	int pos=cnt;
	while(pos>1){//和父节点进行比较即可
		if(q[pos]>q[pos/2])
			swap(q[pos],q[pos/2]);
		pos=pos/2;
	}
}
void pop(){
	if(cnt==0) return;
	q[1]=q[cnt--];//通过和最后一个交换把最大的(节点1)去掉 然后调整大根堆
	int pos=1;//pos记录当前位置  f记录是否交换
	while(pos*2<=cnt){//和子节点比较 子节点可能有两个
		int d=pos*2;
		if(d<cnt&&q[d+1]>q[d]) d++;//左右孩子中比较大的一个
		if(q[pos]<q[d])//自己比孩子小
			swap(q[pos],q[d]);
		pos=d;
	}
}
item top(){//取堆顶
	if(cnt==0){
		return q[0];
	}
	return q[1];
}
bool empty(){//判空
	return cnt==0;
}
	//手写堆完成

double cal_bound(int x,int v){//当前物品编号和剩余体积
	double res=0;
	for(int i=x;i<=n;i++){//剩下的所有物品 能装就装
		if(a[i].w<=v){
			v-=a[i].w;
			res+=a[i].v;
		}
		else{//装不下  拆成一部分装
			res+=a[i].f*v; v=0;
			break;
		}
	}
	return res;
}

void bfs(){//核心过程

	item first;first.bound=cal_bound(0,total);
	push(first);//初始化 装入第一个节点 编号0

	while(!empty()){
		item cur=top();//构建左孩子和右孩子
		//cout<<"level= "<
		pop();
		if(cur.level==n+1){//第一个节点是0 第1个节点是编号1  第n+1个节点就是界限了
			if(res<cur.v){
				res=cur.v;
				respath.assign(cur.path.begin(),cur.path.end());//vector复制
			}
			break;//找到的第一个肯定就是结果
		}


		//左孩子 选择新物品
		int num=cur.level+1,emp_volumn=total-cur.w;
		if(emp_volumn >= a[num].w&&num<=n){
			item lchild; lchild.level=num; lchild.w=cur.w+a[num].w;
			lchild.v=cur.v+a[num].v; lchild.id=a[num].id;
			lchild.path.assign(cur.path.begin(),cur.path.end());
			lchild.path.push_back(a[num].id);
			lchild.bound=cur.v+cal_bound(num,emp_volumn); //左孩子可以直接写成 lchild.bound=cur.bound 结果相等
			push(lchild);
		}

		//右孩子 不选择新物品
		item rchild;
		rchild.level=num;	rchild.id=a[num].id;
		rchild.w=cur.w;		rchild.v=cur.v;
		rchild.path.assign(cur.path.begin(),cur.path.end());
		rchild.bound=cur.v+cal_bound(num+1,emp_volumn);//因为不选择新物品 所以跳过物品num
		push(rchild);
	}


}
int main()
{
     cin>>n>>total;//物品数量 总容量
    for(int i=1;i<=n;i++){
		 cin>>a[i].w>>a[i].v;
		 a[i].f=(double)a[i].v/a[i].w;
		 a[i].id=i;
    }
    sort(a+1,a+1+n,cmp1);
    for(int i=1;i<=n;i++){
		a[i].level=i;
    }
    bfs();
    cout<<res<<endl;
    cout<<"共选取"<<respath.size()<<"个物品"<<endl;
    sort(respath.begin(),respath.end());
    for(int i=0;i<(int)respath.size();i++){
		cout<<respath[i]<<" ";
    }
    return 0;
}

代码:(priority_queue实现版本)

#include 
#include 
#include 
using namespace std;
//分支限界法求01背包
const int N=1005;
struct item{
	int w,v,id,level;//已装入物品的重量 价值  id表示当前物品的原编号 level表示现编号(高度)
	double f,bound;
	vector<int>path;
	item(){
		w=0; v=0; id=0;level=0; f=0; bound=0;
		path.clear();
	}

}a[N];
int total,n,res=0;//背包总容量  物品总数量 题目求的最大价值
vector<int> respath;//记录物品选取情况
bool cmp1(item A,item B){
	return A.f>B.f;
}
bool operator <(const item &A, const item &B){
		return A.bound<B.bound;
}
priority_queue<item>q;
double cal_bound(int x,int v){//当前物品编号和剩余体积
	double res=0;
	for(int i=x;i<=n;i++){
		if(a[i].w<=v){
			v-=a[i].w;
			res+=a[i].v;
		}
		else{
			res+=a[i].f*v; v=0;
			break;
		}
	}
	return res;
}

void bfs(){

	item first;first.bound=cal_bound(0,total);
	q.push(first);

	while(!q.empty()){
		item cur=q.top();//构建左孩子和右孩子
		q.pop();
		if(cur.level==n+1){
			if(res<cur.v){
				res=cur.v;
				respath.assign(cur.path.begin(),cur.path.end());
			}
			break;
		}


		//左孩子 选择新物品
		 int num=cur.level+1,emp_volumn=total-cur.w;
		if(emp_volumn >= a[num].w){
			item lchild; lchild.level=num; lchild.w=cur.w+a[num].w;
			lchild.v=cur.v+a[num].v; lchild.id=a[num].id;
			lchild.path.assign(cur.path.begin(),cur.path.end());
			lchild.path.push_back(a[num].id);
			lchild.bound=cur.v+cal_bound(num,emp_volumn); //左孩子可以直接写成 lchild.bound=cur.bound 结果相等
			q.push(lchild);
		}

		//右孩子 不选择新物品
		item rchild;
		rchild.level=num;	rchild.id=a[num].id;
		rchild.w=cur.w;		rchild.v=cur.v;
		rchild.path.assign(cur.path.begin(),cur.path.end());
		rchild.bound=cur.v+cal_bound(num+1,emp_volumn);//当前物品不选 所以从num+1开始
		q.push(rchild);
	}

}
int main()
{
    cin>>n>>total;
    for(int i=1;i<=n;i++){
		 cin>>a[i].w>>a[i].v;
		 a[i].f=(double)a[i].v/a[i].w;
		 a[i].id=i;
    }
    sort(a+1,a+1+n,cmp1);
    for(int i=1;i<=n;i++){
		a[i].level=i;
    }
    bfs();
    cout<<res<<endl;
    cout<<"共选取"<<respath.size()<<"个物品"<<endl;
    sort(respath.begin(),respath.end());
    for(int i=0;i<(int)respath.size();i++){
		cout<<respath[i]<<" ";
    }
    return 0;
}

你可能感兴趣的:(分支限界法,算法)