首页 > 代码库 > 回溯法——装载问题

回溯法——装载问题

问题描述:

  有一批共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++) {
 6             x[t]=i;
 7             //剪枝函数:约束函数+限界函数   ————>  递归    
 8             if (cw1+wt*i/i<=c1&&cw2+wt*i/i<=c2;) {
 9                 cw1=cw1+i*wt/i;
10                 cw2=cw2+i*wt/i;  
11                 backtrack(t+1);
12             }
13       }
14 }
15