分支界定法 branch-and-bound 分析与实现

来源:互联网 发布:js事件绑定和事件委托 编辑:程序博客网 时间:2024/06/06 19:28

1. 介绍分支界定法之前需要了解一下广度优先搜索breadth-First-search(BFS)

1、从图中某个顶点V0出发,并访问此顶点;以层为顺序,一层一层往下遍历
2、从V0出发,访问V0的各个未曾访问的邻接点W1,W2,…,Wk;然后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点

宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

BFS,其英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。一般的实验里,其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表)list, vector或者是最优值的小根堆大根堆,而被检验过的节点则被放置在被称为 closed 的容器中。(open-closed表)


广度搜索的判断重复如果直接判断十分耗时,我们一般借助哈希表来优化时间复杂度

分支界定法一般是广度优先搜索的优化

2. 分支界定法

    分支界定算法是一种在问题的解空间树上搜索问题的解方法。一般如果有n层, 那么解空间树的叶节点有2 power n, 那么分支界定法就是使用限定条件,不断对这颗解空间树进行剪枝, 直到最后一层。

    分支界定法采用广度优先或者最小耗费优先的方法搜索解空间树。 并且,在分解界定法中, 每一个活结点只有一次机会会成为扩展节点。他的搜索策略是:

   1. 产生当前扩展节点的所有孩子节点, 并且在活结点链表中删除当前拓展节点;对于一般的求最优值和资源问题是二叉树, 对于求最短路径这样的图论问题, 是一般的树

   2. 在产生的孩子节点中,剪掉那些不可能产生可行解(或最优解)的节点;(使用限定条件)

   3.将其余所有的孩子节点加入活节点链表;

   4.从活结点表中选择下一个活结点作为新的扩展节点;

   5. 遇到层结束标记(采取不同的数据结构实现可能不同, 加入新的结束标记)

如此循环,直到找到问题的可行解(或最优解)或者活结点表为空。

    分支界定法的思想是:首先确定目标值的上下界, 边搜索边减掉搜索树的某些枝, 提高搜索效率。

   分支界定法在两个方面加速了算法的搜索效率, 一是在选择扩展节点时, 总是选择一个最小成本的节点, 尽可能早的进入最有可能成为最优解的分支;二是扩展节点的过程,舍弃导致不可行解或导致非最优解的子节点。

 

    选择下一个扩展节点E-节点有如下方式

      FIFO搜索
      LIFO搜索
      优先队列式搜索

举个例子,一个容量为10的集装箱,有重量分别为4,8,5的货物,如何才能装最多

 

3. 使用C++实现FIFO的分支界定算法, 集装箱问题

  现在有一个集装箱, 能装30的容量, 有分别为10, 15, 20的货物, 问如何能最大化集装箱的载重货物??

 我们知道答案为30

下面使用C++实现改算法

#include <iostream>#include <list>using namespace std;#define MAX 3const int CAP=30;//最大容量const int box[MAX]={10,15,20};//三个箱子int main(){    int temp=0,level=-1,best=0;    int curVal=0,parentVal=0,expectVal=0;    list<int> queue;    queue.push_back(-1);//-1表示一层    queue.push_back(parentVal);        do    {        parentVal=queue.front(); //first in first out        queue.pop_front(); //按照FIFO取出第一个元素,并且删除        if(parentVal!=-1)        {            //left child            curVal=parentVal+box[level]; //提高效率的一方面            if(curVal>best&&curVal<=CAP)//边界条件 记录一个最大值 并且条件限定在CAP之内, 不满足条件的剪枝            {                best=curVal;      std::cout<<"bestvalue"<<best<<endl;                //最后一层节点不加入队列                if(level<MAX-1){                    queue.push_back(curVal);}            }                        //right childtemp = 0;            curVal=parentVal;            for(int i=level+1;i<MAX;i++){                temp+=box[i];}            expectVal=curVal+temp; //期望值为不加上当前level的值, 剩下的可能最大的值,提高效率的第二个方面   std::cout<<"expect : "<<expectVal<<endl;            //预计最大值若大于目前最佳值,则加入队列;否则不加入,即剪枝            if(expectVal>best&&level<MAX-1){                queue.push_back(curVal);}for(list<int>::iterator ite = queue.begin(); ite != queue.end(); ite++){std::cout<<" "<<*ite;}        }else//处理层次问题,加入层结束标志,进入下一层{            if(level<MAX-1){                queue.push_back(-1);}            level++;        }    }while(level!=MAX&&queue.empty()!=true);//到最后一层或者是队列为空结束    std::cout<<best<<endl;system("pause");    return 0;}

   如果细分,上面的算法实际上是FIFO通过加入“限界”策略加速搜索

    搜索顺序仍采用FIFO的分支搜索,但当遇到:
    若当前分支的“装载上界”,比现有的最大装载小,则该分支就无需继续搜索。
  “装载上界”,容易求解,就是假设装载当前物品以后的所有物品。

 

 

4. FIFO实现分支搜索

  有两艘船,n个货箱。第一艘船的载重量是c1,第二艘船的载重量是c2wi是货箱i的重量,

   w1+w2+……+wn≤c1+c2

  确定是否有一种可将所有n 个货箱全部装船的方法。若有的话,找出该方法。

   1) 使用FIFO分支搜索实现算法

#include <iostream>#include <queue>using namespace std;int w[100]; int n; int bestw=0;queue<int> queue1;//存储当前或节点void AddLiveNode(int wt,int i){ if (i==n)//是叶子, 需要结束{ if (wt>bestw) bestw=wt;} else             //不是叶子{queue1.push(wt);}}int MaxLoading(int c)   { // 初始化活结点队列,标记分层queue1.push(-1);int level=0;     int currentw=0;    while(!queue1.empty()){  if(currentw!= -1){if(currentw+w[level]<=c)   //左节点,限定条件为current + wi < c1{AddLiveNode(currentw+w[level],level);  //物品i可以装载}AddLiveNode(currentw,level); //右孩子总是可行的,不装载物品i//取下一个E-结点currentw = queue1.front();queue1.pop();cout<<"current"<<currentw<<endl;}else  //到达层的尾部{  if (queue1.empty()) {cout<<"best"<<bestw<<endl;return bestw;}//添加分层标记queue1.push(-1);           //取下一个E-结点 currentw = queue1.front();queue1.pop();cout<<"current next level"<<currentw<<endl;level++;}     //ew的}}void main( ){ int c1,c2;int sum=0;scanf("%d", &c1);scanf("%d", &c2);scanf("%d", &n);cout<<"n"<<n<<endl;for(int i=0;i<n;i++) { scanf("%d", w+i); sum=sum+w[i]; }if (sum<=c1 || sum<=c2)     {    cout<<"need only one ship"<<endl;     return;    }    if (sum>c1+c2)     {    cout<<"no solution"<<endl;   return;    }   MaxLoading(c1);   if ((sum-bestw) <= c2)   {    cout<<"The first ship loading"<<bestw<<endl;   cout<<"The second ship loading"<< sum-bestw<<endl;   }   else    {   cout<<"no solution"<<endl;   }   system("pause");}


5. 优先队列构造最优解

   数据结构设计:

1)要输出解的方案,在搜索过程中仍需要生成解结构树,其结点信息包括指向父结点的指针标识物品取舍(或是父结点的左、右孩子)。 (左节点1, 右节点0)

2)堆结点包括结点优先级信息:结点所在分支的装载上界uweight;堆中无法体现结点的层次信息level),只能存储在结点中;

AddLiveNode用于把活结点加到子树中,并把HeapNode类型的活结点插入最大堆。

3)不同于算法2,由于扩展结点不是按层进行的,计算结点所在分支的装载上界时(expectVal>best),要用数组变量r记录当前层以下的最大重量,这样可随时方便使用各层结点的装载上界。

 

6. 算法复杂度

  不管如何算法的复杂度仍为O(2n)但通过限界策略,并没有搜索子集树中的所有结点,且由于每次都是选取最接近最优解的结点扩展,所以一旦搜索到叶结点作E结点时算法就可结束。算法结束时堆并不一定为空。

 

 

0 0
原创粉丝点击