上一篇我们介绍了回溯法。了解了回溯法的空间状态是树形结构。本篇介绍一个回溯法的一个应用:最优装载问题。
最优装载问题
我们在贪心法中介绍过最优装载问题,本篇是说它的加强版本。由于是加强版本,自然问题也变了,比原先的最优装载问题复杂了一些。原先问题是1艘轮船。而加强版的是2艘轮船
什么是加强版的最优装载问题
有n个集装箱,需要装上两艘载重为c1和c2的轮船。wi为第i个集装箱的重量,且w1+w2+…+wn<=c1+c2。问:是否存在一种合理的装载方案把这n个集装箱装上船?如果有,请给出一种方案。
下面我们给个例子:
假设W=<90,80,40,30,20,12,10>,其中第一艘轮船限重c1=152,第二艘轮船限重c2=130。
我们能找到一种方案使得这n个箱子都能上船。我们可以把1,3,6,7装第一艘船,其余装第二艘船;当然这个解不是唯一的,也可以1,4,5,6装第一艘船,其余装第二艘船。那么这两种情况,是两艘船都不超重而且还能把所有箱子装上,因此这两个解是符合要求的。
下面对问题进行建模:
输入:W=<w1,w2,w3,…,wn>为集装箱重量,c1,c2分别为两艘船的最大载重量。
求是否能将这些箱子再不超重的情况下成功上船。
下面我们用回溯法解这个问题
回溯法解最优装载问题
首先给出一个算法思想:
1、用回溯算法求第一艘轮船的可以装载的最大质量W1
2、如果剩下未装箱的质量<=c2,则存在符合要求的装载方案;否则不存在。
下面我们用递归法进行分析
递归法
根据物品的装与不装,我们可以画一棵子集树来求解。
下面展示子集树画法:
如果能装上,我们令Wi=1,此时安排到左子树上;否则Wi=0,放弃这个物品,从右子树中进行搜索(注意:右子树是包含剩下未考虑的物品)
首先从根出发,考虑将90装上的情况。
90装上后,考虑80的情况。因为90+80=170>c1,则不能装箱。
这个虚线是不能装箱的情况,抛弃W2,让W2=0,因此要从右子树进行搜索
下一个重量40的物品是可以装上第一艘船的,90+40<c1。令W3=1
下面考虑重量30的物品,因为90+40+30>c1,因此不能装箱,要从右子树进行搜索。
重量20的物品是可以上第一艘船的,而此时的装载重量已经是90+40+20=150了
因此后面的箱子都不能再装了。因为再装就要超过第一艘船的最大载重量c1了
然后进行回溯,从右子树进行回溯,我们找到了一个来自左子树的点,即重量20的点,而那个分支上写的是1。所以我们考虑如果不装20,会是什么结果呢?
不装20,那我们就要从这个点的右子树进行搜索
我们可以装上12,也可以装上10。90+40+12+10=152
我们找到了第一个解,按照路径:根—>90->40->12->10这一条路径。这条路径的可装载重量是152=c1,即是最大的可装载重量
当然程序也可以再向上回溯,寻找另外的最优解。下面的分支就不再画了。
下面进行代码分析
关于子集树类型的代码:首先写递归出口,如果到达叶结点,若存在最优解就记录最优解,否则return;
没有到达叶结点,先考虑是否满足约束条件,若满足,则将点加到解数组里面,更新可装载重量,然后进行递归(t+1)。
然后就要考虑进入右子树,首先先将在左子树的可装载重量减掉,然后再进入右子树。就是说如果回溯到上面那个重量20的结点,从那个结点进入右子树,那么首先可装载重量减到20,然后再将这个结点踢出解数组里面,然后才可以进入右子树。右子树也是递归(t+1)
这个子集树的递归代码都基本是这样的套路,多写多练!
下面上代码
public class Loading {
public int index; //货物编号
public int weight; //货物重量
public Loading() {
// TODO Auto-generated constructor stub
}
public Loading(int index, int weight) {
super();
this.index = index;
this.weight = weight;
}
}
public static int nowweight; //装载现重
public static int maxweight=-0x3f3f3f3f;
public static void main(String[] args) {
// TODO Auto-generated method stub
Loading loading[]=new Loading[7];
loading[0]=new Loading(1,90);
loading[1]=new Loading(2,20);
loading[2]=new Loading(3,12);
loading[3]=new Loading(4,10);
loading[4]=new Loading(5,80);
loading[5]=new Loading(6,30);
loading[6]=new Loading(7,40);
int c1=152; //第一艘轮船限重
int c2=130; //第二艘轮船限重
int x[]=new int[loading.length]; //存储当前路径
int bestx[]=new int[loading.length]; //记录当前最优解
RebackLoading(0, loading, x, c1, bestx);
int sum=0; //记录未装箱质量的和
for(int i=0;i<bestx.length;i++)
if(bestx[i]==0)
sum+=loading[i].weight;
if(sum<=c2) {
System.out.print("有方案:");
for(int i=0;i<x.length;i++)
if(bestx[i]==1)
System.out.print(loading[i].index+" ");
System.out.println("装第1艘船,其余装第2艘船");
}
else
System.out.println("不存在一种方案把这些箱子装上船");
}
//回溯法解最优装载问题(递归法)
public static void RebackLoading(int i,Loading loading[],int x[],int c1,int bestx[]) {
//到达叶节点,搜索了整个数组,将所有情况考虑进去了
if(i==loading.length) {
if(nowweight>maxweight) {
for(int j=0;j<loading.length;j++)
bestx[j]=x[j]; //将最优解路径记下来
maxweight=nowweight;
}
return;
}
if(nowweight+loading[i].weight<=c1) {
nowweight+=loading[i].weight;
x[i]=1;
RebackLoading(i+1, loading, x, c1,bestx);
//退回上一分支,将左子树占用空间还回去,准备走右子树
nowweight-=loading[i].weight;
}
//搜索右子树
x[i]=0;
RebackLoading(i+1, loading, x, c1,bestx);
}
下面进行考虑迭代写法
迭代法
我们可以换一种算法思想来解这个问题:
1、用回溯算法求c1-W1,剩余可装载量达到最小
2、如果剩下未装箱的质量<=c2,则存在符合要求的装载方案;否则不存在。
下面直接进行代码分析:
迭代写法还是两层循环,首先先将物品装上(当然也可以先按重量进行升序排序),能装多少装多少,直到出现装不上的情况。
然后用c1-w1得到了剩余可装载量。如果此时的计算出来的剩余可装载量达到最小,则更新最小值。
如果到达叶子结点,那么就要回溯,沿右分支一直回溯到左分支,如果找到左分支了,那么沿它的右分支进行。首先先将刚才计算的剩余可装载量还回去,就是剩余可装载量=剩余可装载量+wi的质量(实际上就是将这个物品踢出去,将被占用的空间还回来)。如果回溯到了根上,则所有情况都已经考虑完毕,则终止循环就可以了。
下面根据这个分析,来编写代码:
public static void main(String[] args) {
// TODO Auto-generated method stub
Loading loading[]=new Loading[7];
loading[0]=new Loading(1,90);
loading[1]=new Loading(2,20);
loading[2]=new Loading(3,12);
loading[3]=new Loading(4,10);
loading[4]=new Loading(5,80);
loading[5]=new Loading(6,30);
loading[6]=new Loading(7,40);
int c1=152; //第一艘轮船限重
int c2=130; //第二艘轮船限重
int x[]=new int[loading.length]; //存储当前路径
int bestx[]=new int[loading.length]; //记录当前最优解
RebackLoading2(loading, x, c1, bestx);
int sum=0; //记录未装箱质量的和
for(int i=0;i<bestx.length;i++)
if(bestx[i]==0)
sum+=loading[i].weight;
if(sum<=c2) {
System.out.print("有方案:");
for(int i=0;i<x.length;i++)
if(bestx[i]==1)
System.out.print(loading[i].index+" ");
System.out.println("装第1艘船,其余装第2艘船");
}
else
System.out.println("不存在一种方案把这些箱子装上船");
}
//回溯法解最优装载问题(迭代法)
public static void RebackLoading2(Loading loading[],int x[],int c1,int bestx[]) {
int weight=c1; //剩下的重量
int minweight=0x3f3f3f3f; //第一艘船最小的剩余重量
int i=0;
while(true) {
while(i<loading.length) { //先判断能不能装上,能装上就装,然后轮船剩余重量会减少,记下路径
if(nowweight+loading[i].weight<=c1) {
nowweight+=loading[i].weight;
weight-=loading[i].weight;
x[i]=1;
i++;
continue;
}
else { //不能装就让在此时的路径为0,
x[i]=0;
i++;
}
}
if(weight<minweight) { //如果船内剩余重量<最小的剩余重量,就进行更新;随后将最优解路径记下来
for(int j=0;j<loading.length;j++)
bestx[j]=x[j];
minweight=weight;
}
//到达叶节点,搜索了整个数组
if(i==loading.length)
i--;
//回溯
while(i>0&&x[i]==0) //沿右分支一直回溯到左分支,如果找到左分支了,那么沿它的右分支进行
i--;
if(x[i]==1) { //沿右分支进行前,先把刚才的计算左分支的剩余量还回去
nowweight-=loading[i].weight;
weight+=loading[i].weight;
x[i]=0;
i++; //进入到右分支
}
//如果i回到了0的位置上,说明回溯到了根。说明,所有情况都已经考虑完毕了。则结束循环
if(i==0)
break;
}
}
下面对最优装载问题进行分析
最优装载问题的分析
这个加强版的最优装载问题的状态空间树是一棵子集树,由于子集树是一棵二叉树。所以这个加强版的最优装载问题的时间复杂度就是O(2^n)
以上就是回溯法的应用——最优装载问题的介绍