遞迴、回溯-裝載問題
之前討論了最優裝載問題的貪心演算法,這裡討論最優裝載問題的一個變形。
1、問題描述:
有一批共n個集裝箱要裝上兩艘載重量分別為c1和c2的輪船,其中集裝箱為wi,且
要求確是否有一個合理的裝載方案可將這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揹包問題:
2、演算法設計
用回溯法解裝載問題時,用子集樹表示其解空間顯然是最合適的。可行性約束函式可剪去不滿足約束條件的子樹。在子集樹的第j+1層的結點Z處,用cw記當前的裝載重量,即,當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)時間,子集樹中結點個數為.故Backtrack所需的計算時間為
。另外Backtrack還需要額外的O(n)的遞迴棧空間。
3、上界函式
對於前面描述的演算法Backtrack,可以引入一個上界函式,用於剪去不含最優解的子樹,從而改進演算法在平均情況下的執行效率,設Z是解空間樹第i層上的當前擴充套件結點。cw是當前載重量,bestw是當前最優載重量;r是剩餘集裝箱的重量,即。定義上界函式為cw+r。在以Z為根的子樹中任一葉結點對應的載重量均不超過cw+r。因此,當cw+r <= bestw時,可將Z的右子樹剪去。
在下面的改進演算法中,引入類Loading的變數r,用於計算上界函式。引入上界函式後,在達到一個葉結點時就不必再檢查該葉結點是否優於當前最優解。因為上界函式使演算法搜尋到的每個葉結點都是當前找到的最優解。雖然改進後的演算法的計算時間複雜度仍為.但在平均情況下改進後的演算法檢查的結點數較少。
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;
}