1. 程式人生 > >遞迴、回溯-裝載問題

遞迴、回溯-裝載問題

之前討論了最優裝載問題的貪心演算法,這裡討論最優裝載問題的一個變形。

1、問題描述:

有一批共n個集裝箱要裝上兩艘載重量分別為c1和c2的輪船,其中集裝箱為wi,且\sum_{i=1}^{n}wi <= c1+c2 

要求確是否有一個合理的裝載方案可將這n個集裝箱裝上這2艘輪船?如果有,找出一種裝載方案。

例如,當n=3,c1=c2=50,且w=[10,40,40]時,可將集裝箱1和2裝上第一艘輪船,而將集裝箱3裝上第二艘輪船:如果w=[20,40,40],則無法將這3個集裝箱都裝上輪船。

輸入:集裝箱的個數n,第一艘船的載重量c1,各個集裝箱的重量wi

輸出:第一艘船的最大載貨量,各個集裝箱是否轉載,裝載輸出1,反之輸出0.

執行結果:

容易證明,如果一個給定的裝載問題有解,則採用下面的策略可以得到最優裝載方案。

(1)首先將第一艘輪船儘可能裝滿;

(2)然後將剩餘的集裝箱裝上第二艘輪船。

將第一艘輪船儘可能裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近c1,由此可知,裝載問題等價於以下特殊的0-1揹包問題:

max\sum_{i=1}^{n}wixi

s.t.\sum_{i=1}^{n}wixi <=c1

xi\in {0,1}, 1<=i<=n

2、演算法設計

用回溯法解裝載問題時,用子集樹表示其解空間顯然是最合適的。可行性約束函式可剪去不滿足約束條件\sum_{i=1}^{n}wixi <= ci的子樹。在子集樹的第j+1層的結點Z處,用cw記當前的裝載重量,即cw = \sum_{i=1}^{j}wixi,當cw > c1時,以結點Z為根的子樹中所有結點都不滿足約束條件,因而該子樹中的解均為不可行解,故可將該子樹剪去。

下面的解裝載問題的回溯法中,演算法MaxLoading返回不超過c的最大子集和。

演算法MaxLoading呼叫遞迴函式Backtrack(1)實現回溯搜尋。Backtrack(i)搜尋子集樹中第i層子樹。類Loading的資料成員。記錄子集樹中結點資訊,以減少傳給Backtrack的引數。cw記錄當前結點所對應的裝載重量,bestw記錄當前最大裝載重量。

在演算法Backtrack中,當i>n時,演算法搜尋至葉結點,其相應的裝載重量為cw。如果cw>bestw,則表示當前解優於當前最優解,此時應更新bestw。

當i  <= n時,當前擴充套件結點Z是子集樹中的內部結點。該結點有 x[i] = 1 和 x[i] = 0兩個兒子結點。其左兒子結點表示x[i] = 1的情形,僅當cw+w[i]<=c時進入左子樹,對左子樹遞迴搜尋。其右兒子結點表示x[i]=0的情形。由於可行結點的右兒子結點總是可行的。故進入右子樹時不需檢查可行性。

演算法Backtrack動態的生成問題的解空間樹。在每個結點處演算法花費O(1)時間,子集樹中結點個數為O(2^{n}).故Backtrack所需的計算時間為

O(2^{n})。另外Backtrack還需要額外的O(n)的遞迴棧空間。

3、上界函式

對於前面描述的演算法Backtrack,可以引入一個上界函式,用於剪去不含最優解的子樹,從而改進演算法在平均情況下的執行效率,設Z是解空間樹第i層上的當前擴充套件結點。cw是當前載重量,bestw是當前最優載重量;r是剩餘集裝箱的重量,即r = \sum_{j=i+1}^{n}wj。定義上界函式為cw+r。在以Z為根的子樹中任一葉結點對應的載重量均不超過cw+r。因此,當cw+r <= bestw時,可將Z的右子樹剪去。

在下面的改進演算法中,引入類Loading的變數r,用於計算上界函式。引入上界函式後,在達到一個葉結點時就不必再檢查該葉結點是否優於當前最優解。因為上界函式使演算法搜尋到的每個葉結點都是當前找到的最優解。雖然改進後的演算法的計算時間複雜度仍為O(2^{n}).但在平均情況下改進後的演算法檢查的結點數較少。

4、構造最優解

為了構造最優解,必須在演算法中記錄與當前最優值相應的當前最優解。為此,在類Loading中增加兩個私有資料成員x和bestx。x用於記錄從根至當前結點的路徑;bestx記錄當前最優解。演算法搜尋到葉結點處,就修正bestx的值。.

template <class Type>
class Loading
{
    template <class T>                                        //類模板中友元函式,加上,T防止重名
    friend T MaxLoading(T*, T, int, int*);
private:
    void Backtrack(int t);
    int n,                                                   //集裝箱數
        *x,                                                  //當前解
        *bestx;                                              //當前最優解
    Type *w,                                                 //集裝箱陣列,存放了每個集裝箱重量
         c,                                                  //第一艘輪船的載重量
         cw,                                                 //當前載重量
         bestw,                                              //當前最優載重量
         r;                                                  //剩餘集裝箱重量
};
//核心函式 對解空間樹回溯搜尋,求得最優值
template <class Type>
void Loading<Type>::Backtrack(int t)
{
    int j;
    //搜尋第t層結點
    if(t > n)                                                //到達葉節點,此時x[1,n]是一個滿足約束條件的可行解
    {
        for(j = 1; j <= n; j++)
            bestx[j] = x[j];
        bestw = cw;
        return;
    }
    r -= w[t];
    //搜尋子樹,此時,t<=n時 當前擴充套件結點是子集樹中的內部結點
    //該結點有x[i]=1和x[i]=0兩個兒子結點
    if(cw + w[t] <= c)  //搜尋左子樹x[i]=1
    {
        x[t] = 1;
        cw += w[t];
        Backtrack(t+1);
        cw -= w[t];
    }

    if(cw + r > bestw)
    {
        x[t] = 0;
        Backtrack(t+1);    //搜尋右子樹x[i]=0
    }

    r += w[t];
}
//負責對類的私有變數初始化,呼叫遞迴函式Backtrack(1)實現回溯搜尋並返回最優值
template <class Type>
Type MaxLoading(Type *w, Type c, int n, int *bestx)
{
    int i;
    Loading<Type> X;
    //初始化X
    X.x = new int[n+1];
    X.bestx = bestx;
    X.w = w;
    X.c = c;
    X.n = n;
    X.cw = 0;
    X.bestw = 0;
    //初始化r
    X.r = 0;
    for(i = 1; i <= n; i++)
        X.r += w[i];
    //計算最優載重量
    X.Backtrack(1);
    delete []X.x;
    //返回最優載重量
    return X.bestw;
}