递归、回溯-0-1背包问题

版权声明:转载请注明出处。 https://blog.csdn.net/baidu_38304645/article/details/83830348

0-1背包问题是子集选取问题。一般情况下,0-1背包问题是NP难的,0-1背包问题的解空间可用子集树表示。解0-1背包问题的回溯法与解装载问题的回溯法十分相似。

在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入其左子树。当右子树有可能包含最优解时才进入右子树搜索,否则将右子树剪去。设r是当前剩余物品价值总和:cp是当前价值;bestp是当前最优价值。当cp+r<=bestp时,可剪去右子树。计算中解的上界的更好方法是剩余物品依其单位重量价值排序,然后依次装入物品,直到装不下时,再装入该物品的一部分而装满背包。由此得到的价值是右子树解的上界。

例如,对于0-1背包问题的一个实例,n=4,c=7,p=[9,10,7,4],w=[3,5,2,1].这个物品的单位重量价值分别为[3,2,3,5,4],以物品单位重量价值的递减序装入物品,先装入物品4,然后装入物品3和1,。装入这3个物品后,剩余的背包容量为1,只能装入0.2的物品2.由此得到一个解为x = [1,0.2,1,1],其相应的价值为22.尽管这不是一个可行解,但可以证明其价值是最优解的上界。因此,对于这个实例,最优值不超过22。

输入:物品的数目n,背包的容量c。各个物品的重量wi,各个物品的价值vi。

输出:装入背包的最大价值。

运行结果:

为了便于计算上界,可先将物品依其单位重量价值从大到小排序,此后只要按顺序考察各物品即可。在实现时,由Bound计算当前结点处的上界。类Knap的数据成员记录解空间树中的结点信息,以减少参数传递及递归调用所需的栈空间。在解空间树的当前扩展结点处。仅当要进入右子树时才计算上界Bound,以判断是否可将右子树剪去。进入左子树时不需计算上界,因为其上界与其父结点的上界相同。

template <class Typew, class Typep>
class Knap
{
    template <class Tw, class Tp>
    friend Tp Knapsack(Tw *, Tp *, Tw, int);
private:
    Typep Bound(int i);
    void BackTrace(int i);
    int n;                                  //装包物品重量
    Typew c,                                //背包容量
          *w,                               //物品重量数组
          cw;                               //当前重量,从根到当前结点所形成的部分解的装包物品重量
    Typep cp,                               //当前价值,从根到当前结点所形成的部分解的装包物品价值
          *p,                               //物品价值数组
          bestp;                            //当前最优价值,已搜索过得部分解空间树的最优装包价值
};
//计算以当前结点为根的子树的价值上界
//计算上界的方法是将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下时,再装入该物品的一部分而装满背包。
template <class Typew, class Typep>
Typep Knap<Typew, Typep>::Bound(int i)
{
    Typew cleft;                            //剩余容量
    Typep b;

    cleft = c - cw;
    b = cp;

    while(i <= n && w[i] <= cleft)          //以物品单位重量价值递减序装入物品
    {
        b += p[i];
        cleft -= w[i];
        i++;
    }
    if(i <= n)                              //装满背包,剩余的容量不足一个,装一部分
        b += p[i]*cleft / w[i];

    return b;
}
//对解空间树回溯搜索,求得最大装包价值
template <class Typew, class Typep>
void Knap<Typew, Typep>::BackTrace(int i)
{
    if(i > n)                               //到达叶节点
    {
        bestp = cp;
        return;
    }

    if(cw + w[i] <= c)                      //满足约束函数,进入左子树
    {
        cw += w[i];
        cp += p[i];
        BackTrace(i+1);
        cw -= w[i];                         //回溯还原
        cp -= p[i];
    }
    if(Bound(i+1) > bestp)                  //满足限界函数,进入右子树
        BackTrace(i+1);
}
//物品
class Object
{
    template <class Tw, class Tp>
    friend Tp Knapsack(Tw *, Tp *, Tw, int);
public:
    bool operator < (const Object &a) const
    {
        return d > a.d;
    }
private:
    int ID;                                     //物品标号
    float d;                                    //单位重量价值
};
template <class Typew, class Typep>
Typep Knapsack(Typew *w, Typep *p, Typew c, int n)
{
    Typew W;
    Typep P;
    Object *Q;
    int i;
    //初始化
    W = 0;
    P = 0;
    Q = new Object[n];
    for(i = 1; i <= n; i++)
    {
        Q[i-1].ID = i;
        Q[i-1].d = 1.0*p[i] / w[i];
        W += w[i];
        P += p[i];
    }
    if(W <= c)                      //能够装入所有物品
        return P;
    sort(Q, Q+n);                   //将n个物品依单位重量价值排序

    Knap<Typew, Typep> K;

    K.c = c;
    K.n = n;
    K.bestp = 0;
    K.cw = 0;
    K.cp = 0;
    K.p = new Typep[n+1];
    K.w = new Typew[n+1];
    for(i = 1; i <= n; i++)
    {
        K.w[i] = w[Q[i-1].ID];
        K.p[i] = p[Q[i-1].ID];
    }
    K.BackTrace(1);
    delete []Q;
    delete []K.p;
    delete []K.w;

    return K.bestp;         //返回最大装包价值
}

2、算法效率

计算上界需要O(n)时间,在最坏情况下有O(2^{n})个右儿子结点需要计算上界,故解0-1背包问题的回溯算法Backtrack所需的计算时间为O(n2^{n}).

猜你喜欢

转载自blog.csdn.net/baidu_38304645/article/details/83830348