分支限界法解决0-1背包问题

开篇

0-1背包问题我们已经说过很多次了,这次是用最近学的分支限界法解决。分支限界法就是利用队列或者优先队列在储存解空间树的活结点,并每次弹出一个作为扩展结点,是一种广度优先遍历,区别于回溯法的深度优先遍历。而优先队列时间复杂度更低,因为我们每次加入一个活结点时,队列都会排序,所以我们出队的结点一定是优先级最高的。下面我们就来说一下如何利用分支限界法解决0-1背包问题。

分支限界法解决0-1背包问题

在回溯法中我们谈到了上界函数这个概念,在分支限界法中也有这个函数,这个函数主要是来约束右节点的。在回溯法中我们的上界函数是:剩余价值+当前价值>当前最优价值,当这个函数满足时我们才有必要将右子节点加入到队列中。但是分支限界法这个上界函数有一些变化,具体什么变化可以往下看。
在使用分支限界的时候我们会首先考虑优先队列,因为优先队列往往能降低复杂度让算法更快。那么我们就要定义优先级。在我们谈论用分支限界法解决装载问题的时候我们也设定了优先级,当时我们的优先级设为当前重量+剩余重量。但是在这里是0-1背包问题,并不是完全背包问题,即物品只能装或者不装,这里我们定义上界函数的时候,假设背包可以装一部分,即当背包装不下一整件物品时我们只装一部分。所以我们定义上界函数为当前重量+剩余物品中单位重量价值最大的物品的平均价值*剩余重量,因此在这里我们可以想到,我们需要按照物体单位重量价值从大到小的顺序排列物品。
于是我们就可以推演出上界函数,如果当前重量+剩余物品中单位重量价值最大的物品的平均价值*剩余重量 > 当前最优价值时说明我们有必要将右节点加入到队列中,否则不加入右节点。
慢慢地我们更新当前最优价值,当前价值,当前背包容量,取下一活结点作为扩展结点,继续进行遍历,直到我们到达了第一个叶结点,那就表明我们已经找到了最优解(因为优先队列每次都会弹出优先级最高的活结点)。
最后一个问题,我们怎样获得最优解路径呢?我们在写代码的时候会写一个bbnode类作为子集树中的结点类,它的成员有parent父节点和bool类型的lchild,指示左节点是否加入活结点。Lchild就是我们记录最优路径的主要标量,如果Lchild=1说明此时我们走了左节点,Lchild=0说明此时我们走了右节点。
但是这不是回溯法,而是分支限界法,我们的数据结构不是什么子集树,而是一个优先队列,怎么把子集树的结点和优先队列中的元素关联起来呢?答案是我们只需要创建一个最大堆,堆结点类中的数据成员包括堆结点当前重量、当前价值、最优价值、还有指向子集树结点的指针以及活结点在树中的层数。
最后我们直接返回最优解和最优解路径。下面我们来看代码里的其他细节,我都写好了注释。

代码

#include <iostream>
using namespace std;
//背包物品类
class Object
{
    friend int Knapsack(int*,int*,int,int,int*);
    public:
        int operator<=(Object a) const{return (d >= a.d);}
    private:
        int ID;
        float d;//单位重量价值
};
//子集树中的结点类
class bbnode
{
    friend Knap<int,int>;
    friend int Knap(int*,int*,int,int,int*);
    private:
        bbnode *parent;
        bool Lchild;
};
//堆中堆结点类
class HeapNode
{
    friend Knap<int,int>;
    public:
        operator int () const {return uprofit;}
    private:
        int uprofit;//结点的价值上界
        int profit;//结点相应的价值
        int weight;//结点相应的重量
        int level;//活结点在子集树中所处的层序号
        bbnode *ptr;//指向活结点在子集树中相应结点的指针
};
//背包类,记录当前背包的最大价值,当前价值以及容量
class Knap
{
    friend int Knapsack(int*,int*,int,int,int*);
    public:
        int MaxKnapsack();
    private:
        MaxHeap<HeapNode<int,int>> *H;//创建一个最大堆
        int Bound(int i);//计算上界函数
        void AddLiveNode(int up,int cp,int cw,bool ch,int lev);//添加活结点进入优先队列
        bbnode *E;//指向扩展结点的指针
        int n;//物品总数
        int *w;//物品重量数组
        int *p;//物品价值数组
        int cw;//当前重量
        int c;//背包容量
        int cp;//当前价值
        int *bestx;//最优解数组
};
int Knap<int,int>::Bound(int i)//计算结点所相应的价值上界,贪心思想
{
    int cleft = c - cw;//剩余容量
    int b = cp;//价值上界,函数返回值
    //以物品单位价值递减顺序排列   补充代码
    while(i <= n && w[i] <= cleft)
    {
        b+=p[i];
        cleft -= w[i];
        i++
    }
    if(i<n)
        b +=  p[i] / w[i] * cleft;
    return b;
}
//将活结点加入优先队列中
void Knap<int,int>::AddLiveNode(int up,int cp,int cw,bool ch,int lev)
{
    bbnode *b = new bbnode;
    b->parent = E;//E指向当前的扩展结点
    b->Lchild = ch;
    HeapNode<int,int>N;
    N.ptr = b;//指向子集树中活结点对应的结点
    N.uprofit = up;//这是优先队列的优先级,当前加入的重量+剩余可装入的最大重量(此最大重量为可加入的单位最大重量)
    N.profit = cp;
    N.weight = cw;
    N.level = lev;
    H->insert(N);//将这个点加入到优先队列中
    //整体结构为堆的结点指向子集树的结点,然后加入堆中
}
//按照优先级遍历子集树,将活结点加入到优先队列中
int Knap<int,int>::MaxKnapsack()
{
    H = new MaxHeap<HeapNode<int,int>>(1000);//声明一个优先队列,内部成员类型为堆结点类型
    bestx = new int [n+1];//记录最优解
    cw = cp = 0;
    int bestp = 0;
    int up = Bound(1);
    int i = 1;
    E = 0;//当前扩展结点为0
    while(i != n + 1)
    {
        //检查当前扩展结点的左儿子结点
        int wt = cw + w[i];
        if(wt <= c)
        {
            if(cp+p[i]>bestp)
            {
                bestp = cp+p[i];
            }
            AddLiveNode(up,cp+p[i],cw+w[i],true,i+1);
        }
        //这里就算的Bound只是为了给右节点一个约束,看是否有必要将右节点加入到活结点队列中
        up = Bound(i+1);    
        if(up>=bestp)
        {
            //右节点有机会加入
            AddLiveNode(up,cp,cw,false,i+1);
        }
        //取下一扩展结点
        HeapNode<int,int> N;
        H->DelMax(N);
        up = N.uprofit;//更新最大价值
        cp = N.profit;//更新当前价值,为新的扩展结点的价值
        cw = N.weight;//更新当前重量,为新的扩展结点的重量
        E = N.ptr;//下一扩展点,子集树中的点
        i = N.lev;
    }
    for(int j = n;j > 0;j--)
    {
        bestx[j]=E->Lchild;//将路径解记录下来
        E = E->parent;//逐渐向上遍历找出路径上的具体添加方案
    }
}
//Knapsack函数完成对输入数据的预处理,我们输入的object类的物品,根据我们的最大上界函数即Bound函数可知
//我们需要将单位重量的价格从大到小排序,从而计算Bound,将其作为优先级
//所以这个函数的作用是将输入的Object类对象排序传递给Knap类对象,返回最大价值
int Knapsack(int p[],int w[],int c,int n,int bestx[])
{
    //初始化
    int W = 0;//装包物品重量
    int P = 0;//装包物品价值
    Object *Q = new Object [n];
    for(int i = 0;i <= n;i++)
    {
        //单位重量价值数组
        Q[i-1].ID = i;
        Q[i-1].d = 1.0 * p[i] / w[i];
        P += p[i];
        W += w[i];
    }
    if(W <= c)
    {
        return P;
    }
    Sort(Q,n);//这里自定义一个函数依单位重量价值排序
    //创建类Knap的数据成员
    Knap<int,int> K;
    K.p = new int [n+1];
    K.w = new int [n+1];
    for(int i = 1;i <= n;i++)
    {
        K.p[i] = p[Q[i-1].ID];
        K.w[i] = w[Q[i-1].ID];//按照单位重量价值排好序的价值和重量数组
    }
    K.cp = 0;
    K.cw = 0;
    K.c = c;
    K.n = n;
    int bestp = K.MaxKnapsack();//调用函数求问题的最优解
    for(int j = 1;j <= n;j++)
    {
        bestx[Q[j-1].ID] = K.bestx[j];
    }
    delete[] Q;
    delete[] K.w;
    delete[] K.p;
    delete[] K.bestx;
    return bestp;
}

总结

分支限界法确实是比回溯法更简单,因为我们广度优先遍历,而且每次找优先级最高的,当达到叶结点时一定是最优的。算法的时间复杂度主要是在计算最优解的过程,这取决于我们达到叶结点的时候经过了多少节点,有可能我们没有遍历完一层,下一层的优先级更高,我们就会更进一层,所以取决于我们中间经历过的结点。树的结点数目为2^n.
但是最后一层我们只会找到一个结点,而最后一层的结点数目应该是2^(n-1).
所以我们的时间复杂度最差应为2^(n-1)(2^n-2^(n-1)),但基本不可能。

  • 10
    点赞
  • 157
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值