回溯法的一个应用:最优装载问题

上一篇我们介绍了回溯法。了解了回溯法的空间状态是树形结构。本篇介绍一个回溯法的一个应用:最优装载问题。

最优装载问题

我们在贪心法中介绍过最优装载问题,本篇是说它的加强版本。由于是加强版本,自然问题也变了,比原先的最优装载问题复杂了一些。原先问题是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装上的情况。
(/localImg/ArtImage/2020/03/2020032621260203.jpg)]
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)

以上就是回溯法的应用——最优装载问题的介绍

                天可补,海可填,南山可移。日月既往,不可复追。《曾国藩语录》
  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值