分支限定法,号称5大常用算法(回溯,贪心,动态规划,分治,分支限定法),一直认为分支限定法比较简单,无非就是宽搜+剪枝,所以练习不多,但是这次想尝试一下却出现了一些值得注意的易错点.
首先介绍下分支限定法,简单来说分支限界法以广度优先或以最小耗费优先的方式搜索解空间树。
举个例子,现在有这样一棵树
其中1,3访问完价值为6
1,2,4访问完价值为5,那么1,2,4这个分支可以省去,因为就算你之后所有的节点都取最好的,也比不过1,3这条已知的分支(它也可以取最好,但是它之前的就比你好)
当然还有你访问时要优先扩展价值较高的点,因为他们更有可能产生优秀的分支,而之后扫描较差的分支就可以直接剪去(而如果下一层先扫描差的,就可能让不良的分支也混进来了)。这里就需要用到一个优先权队列,直接用c++ stl 中的priority。
顺便说下priority,弹出是pop,返回顶部元素是top,而最关键的关键值比较格式如下
struct node { int vis[15];//每一个物品装入或没有 int value;//当前的价值 int sumvalue;//最大的价值 int step;//步数 int weight;//当前使用的重量 bool operator < (const node &a) const { return sumvalue>a.sumvalue;//最小值优先 } };
0/1背包问题。假设有4个物品,其重量分别为(4, 7, 5, 3),价值分别为(40, 42, 25, 12),背包容量W=10。
对于每一个物品,我们都可以选取或者不选取,所以树的深度最多为4.(也有可能提前结束,比如我背包太小,只能装一个)
首先,将给定物品按单位重量价值从大到小排序(这是为了后面的估值),先取单位价值较大的,生成左右两个节点,对于左节点(选取)如果其价值小于上一层的最大值,则不再扩展,对于右节点,如果他的价值小于上一层,则不再扩展,而上限估值则是通过当前价值加上未来可能价值,怎么算未来可能价值呢,由于我们是从小到大排列的,所以
目前价值+(总容量-当前花费容量)*(下一个背包的单位价值)就是上限函数.通过上限函数来从堆中选取元素,为什么要用上限函数,因为如果我们优先扩展较差的一条解,能够排除的分支就很少(除非被排除的比这个解还差才可以),而我们选取价值较高的(注意这里是可能总价值,比如你价值但是你已经快没有空间了(下一次都装不了),而我这一次虽然没有装还可以装好多)。
坑有几个:第一是注意序号是排过序的,所以一开始要用的index下标,
第二是height,因为我们是根据sumvalue入堆的,所以有可能第三层节点在第二层节点之前出来,那么保留最终解t就要是最深的那层解.
最后总结一下,分支限定发每一个节点有一个上界,有一个下界(也就是到这一层的价值),下界一旦小于上一层的最大下界,就淘汰,而上界是用来入堆比较的关键词(就是未来预期的价值,有点类似A*算法)
#include <iostream> #include <queue> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; struct node { int vis[15];//每一个物品装入或没有 int value;//当前的价值 int sumvalue;//最大的价值 int step;//步数 int weight;//当前使用的重量 bool operator < (const node &a) const { return sumvalue<a.sumvalue;//最大值优先 } }; struct node2 { int w; int v; double p; int index; }; node2 e[15]; int n,m; int MAX[512]; int cmp(node2 a,node2 b) { return a.p>b.p; } priority_queue<node>q; int main() { int i,j,s,height; node start,temp,templ,tempr,t; scanf("%d%d",&n,&m);//n为物品数量,m为背包容量 for (i=1;i<=n;i++) { scanf("%d%d",&e[i].w,&e[i].v); e[i].p=e[i].v*1.0/e[i].w;//计算单位质量的价值大小 e[i].index=i; } sort(e+1,e+n+1,cmp);//按照价值从大到小 start.step=0; start.value=0; start.weight=0; memset(start.vis,0,sizeof(start.vis)); memset(MAX,0,sizeof(MAX)); q.push(start); height=0; while(!q.empty()) { temp=q.top(); q.pop(); s=temp.step;//上一层的步数 if (s>height) height=s; if (s==n) { if (temp.value>MAX[n]) { MAX[n]=temp.value; t=temp; } continue; } if (temp.weight+e[s+1].w<=m)//如果可以放入 { templ=temp; templ.step++; templ.vis[e[s+1].index]=1;//使用这一层的物品(注意为了记录原来是第几个,要用index) templ.value=temp.value+e[s+1].v;//当前价值加一 templ.weight=temp.weight+e[s+1].w;//当前重量加一 if (s+1<n)//不是这一层不是最后一层(最后一层没法扩展下一层节点,计算估值防止数组下标越界) templ.sumvalue=templ.value+(int)(m-templ.weight)*e[s+2].p;//计算之后能获得的最大价值(用于下一步进行选择,优先扩展估值高的节点) else templ.sumvalue=templ.value; if (templ.value>=MAX[s])//如果它小于当前的最大值,则直接淘汰(这里应该可以省略,因为只有一个实际可以比较的扩展节点,但是如果多个节点注意保留MAX选择) { q.push(templ); if (templ.value>=MAX[s+1])//如果大于这一层的最大值,那么它是最大值 { if (s+1==height+1) t=templ;//只有到最深处的节点才有可能是优秀节点 MAX[s+1]=templ.value; } } } //接下来假设这个物品不放入 tempr=temp; tempr.step++; tempr.vis[e[s+1].index]=0; tempr.value=tempr.value; tempr.weight=tempr.weight; if (s+1<n) tempr.sumvalue=tempr.value+(int)(m-tempr.weight)*e[s+2].p; else tempr.sumvalue=tempr.value; if (tempr.value>=MAX[s])//如果值小于上一层的最大值,那么直接淘汰 q.push(tempr); if (MAX[s+1]==0)//若这一层节点都是不取,则直接取上一层最大值为最大值 MAX[s+1]=tempr.value; } for (i=1;i<=n;i++) if (t.vis[i]==1) printf("第%d个物品要装入\n",i); printf("最大价值为:%d\n",MAX[n]); return 0; }