算法设计与分析的作业,老实说,这个方法挺强的,但是网上的参考基本都是指针+课设,而且解释不清晰,真心难参考····,这里我尽可能简单的重述思路和代码。
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,记录结果,跳出,结束。
整理思路
比较麻烦的地方就是要记录的东西太多了:
每个物品,记录原序号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;
}