1. 程式人生 > >遞迴、回溯-0-1揹包問題

遞迴、回溯-0-1揹包問題

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}).