問題描述:
有一批共n個集裝箱要裝上2艘載重量分別為c1和c2的輪船,其中集裝箱i的重量是wi,且不能超,即Σwi<=c1+c2。
算法思想:
——在給定的裝載問題有解的情況下
最優裝載方案: 首先將第一艘輪船盡可能的裝滿;
然后將剩余的集裝箱裝上第二艘輪船。
將第一艘輪船盡可能的裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近c1。
算法設計:
先考慮裝載一艘輪船的情況,依次討論每個集裝箱的裝載情況,共分為兩種,要么裝(1),要么不裝(0),因此很明顯其解空間樹可以用子集樹來表示。
在算法Maxloading中,返回不超過c的最大子集和,但是並沒有給出到達這個最大子集和的相應子集,稍后完善。
在算法Maxloading中,調用遞歸函數Backtrack(1)實現回溯搜索。Backtrack(i)搜索子集樹中的第i層子樹。
在算法Backtrack中,當i>n時,算法搜索到葉結點,其相應的載重量為cw,如果cw>bestw,則表示當前解優於當前的最優解,此時應該更新bestw。
算法Backtrack動態地生成問題的解空間樹。在每個結點處算法花費O(1)時間。子集樹中結點個數為O(2^n),故Backtrack所需的時間為O(2^n)。另外Backtrack還需要額外的O(n)的遞歸棧空間。
算法描述:
1 template <class Type> 2 class Loading 3 { 4 friend Type MaxLoading(Type [],Type,int); 5 private: 6 void Backtrack(int i); 7 int n; //集裝箱數目 8 Type * w, //集裝箱重量數組 9 c, //第一艘輪船的載重量 10 cw, //當前載重量 11 bestw; //當前最優載重量 12 }; 13 14 template <class Type> 15 void Loading<Type>::Backtrack(int i) //回溯搜索 16 { //搜索第i層結點 17 if(i>n) //到達葉結點 18 { 19 if(cw>bestw) 20 bestw = cw; 21 return; 22 } 23 if(cw+w[i] <= c) //搜索子樹 24 { 25 cw += w[i]; //當前載重量增加正考慮對象的重量 26 Backtrack(i+1); 27 cw -= w[i]; //遞歸返回上一層時,記得減去剛考慮對象的集裝箱重量 28 } 29 Backtrack(i+1); //遞歸搜索第i+1層 30 } 31 32 template <class Type> 33 Type MaxLoading(Type w[],Type c,int n) //返回最優載重量 34 { 35 Loading<Type> X; //初始化 36 X.w = w; 37 X.c = c; 38 X.n = n; 39 X.bestw = 0; //當前最優載重量的初值賦為0 40 X.cw = 0; 41 X.Backtrack(1); //計算最優載重量————調用遞歸函數,實現回溯搜索 42 return X.bestw; 43 }
上界函數:
引入剪枝函數,用於剪去不含最優解的子樹:即當cw(當前載重量)+r(未考察對象的總重量)<bestw(當前的最優載重量)時當前子樹不可能包含最優解,直接減掉。
1 template <class Type> 2 class Loading 3 { 4 friend Type MaxLoading(Type [],Type,int); 5 private: 6 void Backtrack(int i); 7 int n; 8 Type * w, 9 c, 10 cw, 11 bestw, 12 r;//剩余集裝箱重量————未考察過的集裝箱的重量,並非沒有裝載的集裝箱重量 13 }; 14 template <class Type> 15 void Loading<Type>::Backtrack(int i) 16 { 17 if(i>n) 18 { 19 if(cw>bestw) 20 bestw = cw; 21 return; 22 } 23 r-=w[i]; //計算剩余(未考察)的集裝箱的重量,減去當前考察過的對象的重量 24 if(cw+w[i] <= c) 25 { 26 cw += w[i]; 27 Backtrack(i+1); 28 cw -= w[i]; 29 } 30 Backtrack(i+1); 31 r+=w[i]; //遞歸回退返回上一層時,記得修改r的當前值,如果得不到最優解,再取消當前考察的集裝箱,標記為未選,因此剩余容量要再加上當前集裝箱重量 32 } 33 template <class Type> 34 Type MaxLoading(Type w[],Type c,int n) 35 { 36 Loading<Type> X; //初始化 37 X.w = w; 38 X.c = c; 39 X.n = n; 40 X.bestw = 0; 41 X.cw = 0; 42 X.r = 0; //初始化r 43 for(int i=1;i<=n;i++) //計算總共的剩余(當前為考察過的)集裝箱重量 44 X.r += w[i]; 45 X.Backtrack(1); 46 return X.bestw; 47 }
構造最優解:
為了構造最優解,必須在算法中保存最優解的記錄。因此需要兩個成員數組 x ,bestx,一個用於記錄當前的選擇,一個用於記錄最優記錄。
改進后的算法描述如下:
1 template <class Type> 2 class Loading 3 { 4 friend Type MaxLoading(Type [],Type,int); 5 private: 6 void Backtrack(int i); 7 int n, 8 * x, //當前解 9 * bestx; //當前最優解 10 Type * w, //集裝箱重量數組 11 c, //第一艘輪船的載重量 12 cw, //當前載重量 13 bestw, //當前最優載重量 14 r; //剩余集裝箱重量————未考慮過的集裝箱重量,並非沒有裝載的集裝箱重量 15 }; 16 template <class Type> 17 void Loading<Type>::Backtrack(int i) 18 { //搜索第i層結點 19 if(i>n) //到達葉子結點 20 { 21 if(cw>bestw) 22 { 23 for(j=1;j<=n;j++) 24 bestx[j] = x[j]; 25 bestw = cw; 26 } 27 return; 28 } //搜索子樹 29 r-=w[i]; //計算剩余(未考慮過)集裝箱的重量,減去當前考慮過的集裝箱重量 30 if(cw+w[i] <= c) //搜索左子樹 31 { 32 x[i] =1; 33 cw += w[i]; 34 Backtrack(i+1); 35 cw -= w[i]; 36 } 37 if(cw+r > bestw) //搜索右子樹——————剪枝函數 38 { 39 x[i] = 0; 40 Backtrack(i+1); 41 } 42 r+=w[i]; //遞歸返回上一層時,記得修改r的值,如果取不到最優解,再取消當前考慮的集裝箱,標記為未選,因此剩余容量要再加上當前集裝箱重量 43 } 44 template <class Type> 45 Type MaxLoading(Type w[],Type c,int n) 46 { 47 Loading<Type> X; 48 X.w = w; 49 X.c = c; 50 X.n = n; 51 X.bestx = bestx; 52 X.bestw = 0; 53 X.cw = 0; 54 X.r = 0; 55 for(int i=1;i<=n;i++) //計算總共的剩余(當前為考察過的)集裝箱重量
56 X.r += w[i]; 57 X.Backtrack(1); 58 delete []X,x; 59 return X.bestw; 60 }
構造最優解的另一種經典方法:
1 void backtrack(int t) 2 { 3 if(t>n) 4 output(x); 5 else 6 { 7 for(i-0;i<=1;i++) 8 { 9 x[t]=i; 10 if(cw+i*wt<=c) 11 { 12 cw=cw+i*wt; 13 backtrack(t+1); 14 } 15 } 16 } 17 }
迭代回溯方式:
利用數組x所含的信息,可將上面方法表示成非遞歸的形式。省去O(n)遞歸棧空間。非遞歸迭代回溯法描述如下:
1 template <class Type> 2 Type MaxLoading(Type w[],Type c,int n,int bestx[]) 3 { 4 //迭代回溯法,返回最優裝載量及其相應解,初始化根節點 5 int i =1; //當前層 x[1:i-1]為當前路徑 6 int *x = new int[n+1]; 7 Type bestw = 0, //當前最優載重量 8 cw = 0, //當前載重量 9 r = 0; //剩余(未考慮過的)集裝箱重量 10 for(int j=1;j<=n;j++) 11 r+=w[j]; 12 while(true) 13 { //進入子樹 14 while(i<=n && cw+w[i]<=c) 15 { //搜索左子樹 16 r -= w[i]; 17 cw +=w[i]; 18 x[i] =1; 19 i++; 20 } 21 if(i>n) //到達葉子結點 22 { 23 for(int j=1;j<=n;j++) 24 bestx[j] = x[j]; 25 bestw = cw; 26 } 27 else //進入右子樹 28 { 29 r -= w[i]; 30 x[i] = 0; 31 i++; 32 } 33 while(cw+w[i] <= bestw) 34 { //剪枝回溯 35 i--; 36 while(i>0 && !x[i]) 37 { //從右子樹返回 38 r+=w[i]; 39 i--; 40 } 41 if(i == 0) 42 { 43 delete[] x; 44 return bestw; 45 } //進入右子樹 46 x[i] =0; 47 cw -= w[i]; 48 i++; 49 } 50 } 51 }
注:上面的解法實際上就是針對一艘輪船的最優轉載
針對於多艘輪船就不一定是最優解了。。。。。
如果是多艘輪船的裝載問題:
同樣選擇回溯法解決——子集樹,只是每個集裝箱的選擇范圍不再僅僅是裝否兩種考慮情況,而變為不裝抑或是裝載到某個編號的輪船上等多種考慮范圍。
例如:n=3,c1=c2=50,w=[10,40,40],所以對於3個集裝箱中的任何一個都有3種選擇,不裝、轉載到1號輪船和轉載到2號輪船。
基本框架如下:
1 void backtrack (int t) //t:代表待考察的對象 2 { 3 if (t>n) output(x); //n:考察對象的個數 4 else 5 for (int i=0;i<=2;i++) { //控制分支的數目,此處應有3個分支,0、1、2分別代表不裝,裝載到1號輪船,裝載到2號輪船 6 x[t]=i; 7 if (constraint(t)&&bound(t)) backtrack(t+1); //剪枝函數:約束函數+限界函數 ————> 遞歸 8 } 9 }
約束函數為:
cw1+wt*i/i<=c1&&cw2+wt*i/i<=c2;
具體實現為:
1 void backtrack (int t) //t:代表待考察的對象 2 { 3 if (t>n) output(x); //n:考察對象的個數 4 else 5 for (int i=0;i<=2;i++) { //控制分支的數目,此處應有3個分支,0、1、2分別代表不裝,裝載到1號輪船,裝載到2號輪船 6 x[t]=i; 7 //剪枝函數:約束函數+限界函數 ————> 遞歸 8 if (i==1? cw1+wt*i/i<=c1:cw2+wt*i/i<=c2) { //滿足每艘輪船的載重要求,小於其載重量 9 if(i==1) cw1=cw1+i*wt/i; //當前載重的改變 10 if(i==2) cw2=cw2+i*wt/i; 11 backtrack(t+1); 12 } 13 } 14 } 15
