分支限界法之最优装载问题

开篇

装载问题已经说过很多种解决方案了,在这里就不再次重复问题了,动态规划,贪心,回溯法都可以解决,今天我们来说一种新的方法——分支限界法。

分支限界法

什么是分支限界法呢?分支限界法类似回溯法,也是在问题的解空间上搜索问题解的算法。一般情况下,分支限界法与回溯法的求解目标不同,回溯法的求解目标是找出解空间中满足约束条件的所有解,而分支限界法的求解目标是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或者极小的解,即在某种意义下的最优解。
这听起来有点像广度优先遍历,其实它的实质基本就是广度优先遍历或者以最小耗费优先的方式搜索解空间。分支限界法的搜索策略是,在扩展结点处,先生成其所有儿子结点(分支),再从当前的活结点表中选择下一个扩展结点。为了有效地选择下一扩展结点,加速搜索过程,在每个活结点处,计算一个函数值(限界),并根据函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间上有最优解的分支推进,以便尽快地找出一个最优解。这就是分支限界法。
分支限界法的基本思想
分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
在分支限界法中,每个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
分支限界法的解题方法有两种,是利用的不同的数据结构:
1.队列式(FIFO)分支限界法
队列式分支限界法将活结点表组织成一个队列,并按队列的先进先出原则选下一个结点为当前扩展结点。
2.优先队列式分支限界法
优先队列式的分支限界法将活结点表组织成一个优先队列,并按优先队列中规定的结点优先级选取优先级最高的下一个结点成为当前扩展结点。
不难看出,优先队列和普通队列的区别就在于,我们不忙地依次将元素出队,而是选取最优出队。第一种使用的数据结构是普通队列;第二种是优先队列,即我们会创建一个大顶堆,完成自动排序。
优先队列中规定的结点优先级常用一个与该结点相关的数值p来表示。结点优先级的高低与p值的大小相关。最大优先队列规定p值较大的结点优先级较高。在算法实现时,通常使用最大堆来实现最大优先队列,体现最大效益优先的原则。类似地,用最小优先队列规定p值较小的结点优先级较高。在算法实现时,通常用最小堆来实现最小优先队列,用最小堆的Deletemin运算抽取堆中下一个结点成为当前扩展结点,体现最小费用优先的原则。
分支限界的大致思路我们了解了,那下面来看一个具体的例子——装载问题。

装载问题

装载问题已经很熟悉了我就不多说了,就直接说两种不同的解决方法——队列式和优先队列式。
1.队列式分支限界法
既然要求尽可能将所有物品都搬入船上,我们的解空间就一定是一个子集树,我们只需要找其中一个最优方案即可。
我们先开辟一个队列,这个队列存放的就是子集树的每一个扩展结点(注意:叶子结点不加入队列中)。我们会采用广度优先遍历的方法,首先在队列中添加一个0,表示这一层的结点都添加完成了。(这里大家体会一下,0其实就是一个标记,标志着我们本层已经全部加入队列了,没有什么实质的意义)。
然后我们将根节点作为我们的扩展结点,将其儿子结点全部加入到队列中,此时我们将队首元素删除,依次对其儿子结点进行处理。处理方式为计算在当前位置(装入或者不装入)的情况下,载重量是多少,是否大于最优载重量,如果大于就对最优载重量进行更新。
假如我们现在处理完了左孩子结点,我们就要按照最开始的基本思想,将左孩子结点的所有孩子结点统统加入到队列中,然后将队首元素删除。
依次进行上述操作,直至队列为空。
每当我们队首元素为0且队列不为空时,说明这一层我们已经访问结束了,我们需要在队列尾步添加0.
那如果队首元素为0且队列已经空了的话,我们就可以break结束了,因为所有点都已经作为过一次扩展结点了。
优化方式
其实这个方法其中还是有很多可以优化的地方的。比如我们在回溯中也说到过,如果当前载重量+剩余物品总重量>当前最优载重量,说明我们可以访问右子树;否则我们直接将右子树剪枝,原因也很简单,如果当前载重量+剩余重量<当前最优载重量,那如果我们走右子树的话,说明此时这个物品我们也不将其搬上船,当然不可能获得最优载重量,所以干脆剪枝掉。
还有一处可以优化,我们的bestw应该随时被更新。正常情况下,bestw是直到遍历到了第一个叶子结点才进行更新的,但是这样的话bestw一开始恒为0,一定满足当前载重量+剩余物品重量>bestw,这样的话我们上面所说的简直操作根本不起作用,所以我们要随时更新bestw,只要遇到了wt>bestw,直接更新(wt=当前层物品重量+扩展结点载重量)
代码

//队列结点的声明
template<class Type>
class QNode{
    friend void EnQueue(Queue<QNode<Type> *> &,Type,int,int,Type,QNode<Type>*,QNode<Type>*&,int *,bool);
    friend Type MaxLoading(Type*,Type,int,int*);
    private:
        QNode *parent;//指向父结点的指针
        bool Lchild;//左耳子标志
        Type weight;//结点所对应的载重量
};
//结点入队列函数
void EnQueue(Queue<QNode<Type> *> &Q,Type wt,int i,int n,Type bestw,QNode<Type>*E,QNode<Type> * &bestE,int bestx[],bool ch)
{
    if(i == n)//可行结点
    {
        if(wt == bestw)
        {
            bestE = E;//当前最优载重量
            bestx[n] = ch;
        }
        return;
    }
    QNode<Type> *b;
    b = new QNode<Type>;
    b->weight = wt;
    b->Lchild = ch;
    b->parent = E;
    Q.Add(b);
}
//最大装载函数
Type MaxLoading(Type w,Type c,int n,int bestx[])//队列式分支限界法,返回最优载重量,bestx返回最优解
//初始化
{
    Queue<QNode<Type> *> Q;//活结点队列
    Q.Add(0);
    int i = 1;//当前扩展结点的层数
    Type *Ew = 0,//扩展结点对应的载重量
        bestw = 0,//当前最优载重量
        r = 0;//剩余重量
    //初始化剩余重量
    for(int j = 0;j <= n;j++)
        r += w[j];
    QNode<Type> *E = 0,//当前扩展结点指针
                *bestE = 0;//当前最优扩展结点指针
    while(true)
    {
        Type wt = w[i] + Ew;//当前扩展结点载重量+下一物品重量
        if(wt <= c)
        {  
            if(wt > bestw)
            {
                bestw = wt;
            }
            EnQueue(Q,wt,i,n,bestw,E,bestE,bestx,true);
        }
        //检查右儿子结点
        //此处剪枝,如果剩余节点重量与当前相加都小于bestw,我们将其剪枝
        if(Ew+r>bestw)
        {
            EnQueue(Q,Ew,i,n,bestw,E,bestE,bestx,false);
        }
        //取下一扩展结点
        Q.Delete(E);
        if(!E)
        {
            if(Q.IsEmpty())
                break;
            Q.Add(0);
            Q.Delete(E);
            i++;
            r -= w[i];
        }
        Ew = E->weight;//刚取出的扩展结点的载重量
    }
    //构造当前最优解
    //从n-1开始遍历,因为n已经在入队操作的时候处理完了
    for(int j = n-1;j > 0;j--)
    {
        bestx[j] = bestE->Lchild;
        bestE = bestE->parent;
    }
    return bestw;
}

2.优先队列式分支限界法
优先式队列大家都懂的,就是最大或者最小堆。比如本题所使用的是最大优先队列,堆顶元素一定是所有元素中最大的元素。我们插入一个元素进堆,堆可以完成自动排序,我们就可以利用这一点来简化我们的算法。
基本思路和普通队列相似,但是既然是优先队列,总需要有一个衡量优先级的标准吧。那标准应该是什么呢?这里我们选取该扩展结点下面的剩余结点重量作为优先级,剩余结点重量大的优先级高。
所以我们先用一个数组存储每一层下面的优先级——即剩余物品重量。每次找出优先级最高的出队,对其进行普通队列的操作,即将其儿子结点进行处理,并加入队列设置优先级。然后对其儿子结点进行访问,设置扩展结点重量,并加入队列中,将其本身从队列中删除(DeleteMax)
然后再继续选最高优先级->选儿子->删除最高->儿子加队列->选最高->选儿子->删除最高->儿子加队列…依次进行。
这种方法什么时候停止呢?答案是当我们遇到第一个叶子节点就终止,因为我们的数据结构是最大优先队列,所以我们每次都找优先级最大的话,当我们第一次遍历到叶子结点,我们能保证沿途都是优先级最高的,所以此时可以结束了,我们可以确保找到了最优解(类似贪心法)。
还有一些细节需要说明一下,我们需要创建一个解空间子集树节点的类bbnode,它的属性有指向扩展结点父节点的指针,以及该扩展结点左孩子的标志(左孩子是否将被作为最优解路径上的一个结点)。
还要声明一个堆结点,既然堆需要根据优先级自动排序,那么内部属性一定包含优先级。其次还应该有结点当前层数,以及一个指向子集树结点的指针,方便我们快速找出下一个扩展结点,以及最后得出最优解的路径。
这里大家可能会问,我们应该如何找出下一扩展结点呢?这就是我们的另一个函数,向堆中添加堆结点的函数。在这个函数中,我们会以上一扩展结点E作为参数,函数内部新创建一个bbnode结点,即子集树中结点,将这个结点的父指针指向E,那这个结点就是我们的下一扩展结点。
代码

//子集树空间结点类型
template<class Type>
class bbnode
{
    friend void AddLiveNode(MaxHeap<HeapNode<int>> &,bbnode *,int,bool,int);
    friend int MaxLoading(int*,int,int,int*);
    friend class AdjacencyGraph;//邻接矩阵
    private:
        bbnode *parent;//指向父节点的指针
        bool Lchild;//左儿子结点标志
};
class HeapNode
{
    friend void AddLiveNode(MaxHeap<HeapNode<Type>> &,bbnode *,Type,bool,int);
    friend Type MaxLoading(Type *,Type,int,int*);
    public:
        operator Type () const{return uweight;}
    private:
        bbnode *ptr;//指向活结点在子集树中相应结点的指针
        Type uweight;//活结点优先级(上界)
        int level;//活结点在子集树中所处的层序号
};
//将活结点加入到表示活结点优先队列的最大堆H中
void AddLiveNode(MaxHeap<HeapNode<Type>> &H,bbnode *E,Type wt,bool ch,int lev)
{
    bbnode *b = new bbnode;
    b->parent = E;
    b->Lchild = ch;
    HeapNode<Type>N;
    N.ptr = b;
    N.uweight = wt;
    N.level = lev;
    H.Insert(N);
}
//优先队列式分支限界法,返回最优载重量,bestx返回最优解
//优先级是当前载重量+剩余重量
Type MaxLoading(Type w[],Type c,int n,int bestx[])
{
    MaxHeap<HeapNode<Type>> H(1000);//定义最大堆的容量为1000
    Type *r = new Type [n+1];//剩余重量
    r[n] = 0;
    for(int j = n - 1;j > 0;j--)
    {
        r[j] = r[j+1] + w[j+1];
    }
    //初始化
    int i = 1;//当前扩展结点所在的层
    bbnode *E = 0;//当前扩展结点
    Type Ew = 0;//扩展结点所相应的载重量
    //搜索子集空间树
    while(i != n + 1)//非叶子节点
    {
        //检查当前扩展结点的儿子节点
        if(Ew+w[i] <= c)
        {
            AddLiveNode(H,E,Ew+w[i]+r[i],true,i+1);//左儿子结点为可行结点
        }
        AddLiveNode(H,E,Ew+r[i],false,i+1);//右儿子结点
        HeapNode<Type>N;//取下一扩展结点
        H.DeleteMax(N);//下一扩展结点,将最大值删去
        i = N.level;
        E = N.ptr;
        Ew = N.uweight - r[i-1];//当前优先级为上一优先级-上一结点重量
    }
    //构造当前最优解,类似回溯的过程
    for(int j = n;j > 0;j--)
    {
        bestx[j] = E->Lchild;
        E = E->parent;
    }
    return Ew;
}

总结

分支限界法和回溯法很相似,但是分支限界是广度优先遍历,回溯法是深度优先遍历。
且分支限界法只要求我们找一种最优,回溯法要求我们将解全部找出。

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