利用DSF深度优先搜索来解容器倒水问题

28 篇文章 2 订阅

在一些面试算法或智力题中,时不时会遇到容器倒水的问题,例如,有三个容器,分别是10升,7升,4升,7升和4升的容器装满了水,10升容器是空的,如果将容器a中的水倒入容器b时,必须使得a中的水全部倒完,或者b被倒满,问有没有一种倒水序列,使得7升容器或4升容器中只有2升的水。


这个问题怎么会跟图论的深度优先搜索联系起来呢。如果我们把三个容器的水量和容量状态当作一个点,例如初始时刻[10(empty:10, water:0), 7(empty:0, water:7), 4(empty:0, water: 4)] 对应一个点A,进行一次倒水操作,例如将7升容器的水倒入10升容器中后,容器容量状态为[10(empty:3, water:7), 7(empty:7, water:0), 4(empty:0, water:4)] 则对应一个点B,这两个点之间就有一条有向边 A->B. 因此该问题的处理方法就是查找从初始点A开始,进行深度优先搜索,看是否有一条路径,使得从点A, 可达到点C,点C代表中7升容器或4升容器装有两升的水。


在DSF遍历中,需要处理一个问题,那就是遍历中访问一个已经被访问的节点,即遇到环或回路的时候,要停止继续搜索。对应于该问题,也存在对应的回路情形,例如将7升的水倒入10升的容器,接着又将10升容器中的水倒回7升容器,这就形成一个回路,如果程序中不处理这种情况,那就回出现死循环。


接下来我们看看代码设计,首先用一个类来表示容器:

public class Container {
	public int water;
	public int empty;
	
	public Container() {
		water = 0;
		empty = 0;
		
	}
	
	public Container(Container otherContainer) {
		water = otherContainer.water;
		empty = otherContainer.empty;
	}
	
	@Override
	public boolean equals(Object otherContainer) {
		if (this.water == ((Container)otherContainer).water && this.empty == ((Container)otherContainer).empty) {
			return true;
		}
		
		return false;
	}
}
water 表示容器中有多少水,empty 是容器的容量,Container 的包含3个元素的数组可以用来表示一个图节点, 代码如下:

public class Vertice {

	private Container[] containers = new Container[3];
	public Vertice(Container[] containers) {
		for (int i = 0; i < 3; i++) {
			this.containers[i] = new Container(containers[i]);
		}
	}
	
	@Override
	public boolean equals(Object obj ) {
		if (!(obj instanceof Vertice)) {
			return false;
		}
		
		Vertice otherContainers = (Vertice)obj;
		if (containers.length != otherContainers.getContainers().length) {
			return false;
		}
		else {
			for (int i = 0; i < containers.length; i++) {
				if (!containers[i].equals(otherContainers.getContainers()[i])) {
					return false;
				}
			}
		}
			
		return true;
	}
	
	public Container[] getContainers() {
		return containers;
	}
}

接下来,我们再看看程序的逻辑代码:所以的主逻辑代码都通过类 DSFPollWater 来实现,先给出代码,然后我再逐步分析:


<pre name="code" class="java">import java.util.Stack;


public class DSFPollWater {
	private Container[] containers = new Container[3];
	private Stack<Vertice> containerStack = new Stack<Vertice>();
    private boolean stackPrinted = false;
	
	public DSFPollWater() {
		initContainers();
	}
	
	private void initContainers() {
		
		for (int i = 0; i < 3; i++) {
			containers[i] = new Container();
		}
		
		containers[0].empty = 10;
		containers[0].water = 0;
		
		containers[1].empty = 0;
		containers[1].water = 7;
		
		containers[2].empty = 0;
		containers[2].water = 4;
	}
	
	
	public void dsfPollingWater() {
	
		if (containerStateExisted()) {
			return;
		}
	
		for (int i = 0; i < containers.length; i++) {
			for (int j = 0; j < containers.length; j++) {
				if (j != i) {
					saveContainerState();	
					boolean bPollResult = pollWater(containers[i], containers[j]);
					if (isSuccessed() && stackPrinted == false) {
						System.out.println("--ok--");
						printStack();
						return;
					}
					
					if (bPollResult) {
					    dsfPollingWater(); 
					}
					
					restoreContainerState();
				}
			}		
		}	
	}
	
	private void printStack() {
		
		for (int i = 0; i < containerStack.size(); i++) {
			Vertice consArr =  containerStack.get(i);
			Container[] cons = consArr.getContainers();
			System.out.print("(");
			for (int j = 0; j < cons.length; j++) {
				System.out.print(cons[j].empty + cons[j].water + "[empty:" + cons[j].empty + ", water:" + cons[j].water + "]" );
				if (j < cons.length - 1) {
					System.out.print(",");
				}
			}
			System.out.print(")");
			
			if (i < containerStack.size() - 1) {
				System.out.print("->");
			}
			
			System.out.print("\n");
		}
		
		stackPrinted = true;
	}
	
	private void saveContainerState() {
		Container[] backupContainers = new Container[]{containers[0], containers[1], containers[2]};
		Vertice containerArr = new Vertice(backupContainers);
		containerStack.push(containerArr);
	}
	
	private void restoreContainerState() {
		if (containerStack.empty() == false) {
			containers = containerStack.pop().getContainers();
		}
	}
	
	public boolean isSuccessed() {
		for (int i = 1; i < containers.length; i++) {
			if (containers[i].water == 2) {
<p class="p1">                                saveContainerState();</p>				return true;
			}
		}
		
		return false;
	}
	
	private boolean pollWater(Container from, Container to) {
		if (to.empty == 0) {
			return false;
		}
		
		int volumn = from.water >= to.empty ? to.empty : from.water;
	
		from.water -= volumn;
		from.empty += volumn;
		to.water += volumn;
		to.empty -= volumn;
		
	
		return true;
	}
	
	private boolean containerStateExisted() {
		Vertice containerArr = new Vertice(containers);
		if (containerStack.contains(containerArr)) {
			return true;
		}
		
		return false;
	}
	
	public static void main(String[] args) {
		DSFPollWater dsfPollWater = new DSFPollWater();
		dsfPollWater.dsfPollingWater();
	}
}


 


首先,通过initContainers 来初始化三个容器的状态, dsfPollingWater 是进行深度优先便利的主函数,在函数开始,调用containerStateExisted 来判断当前容器状态是否已经存在,也相当于遍历时是否遇到了已经遍历过的点,举个例子, 初始状态是:[10(empty:10, water:0), 7(empty:0, water:7), 4(empty:0, water: 4)],接下来,将7升容器中的水全部倒入10升容器,那么状态转换为[10(empty:3, water:7), 7(empty:7, water:0), 4(empty:0, water:4)],如果接下来做的是将10升容器中的水倒入7升容器,那么初始状态又出现了,这就意味着我们进入了环回路,如果不处理这种情况的话,程序会陷入死循环,containerStateExisted 函数就是用于判断这种情况,如果出现回路,递归要立马返回,以便开始新的搜索。接下来的两个for 循环是为了模拟容器倒水情形,三个容器,倒入的方法有:容器1倒入容器2,容器1倒入容器3,容器2 倒入容器1.....,总共有9种倒法,两个for循环就是执行这9种倒水情形。


进入循环,先执行saveContainerState(), 由于执行倒水动作后,容器的存水状况会发生改变,因此在执行倒水前,会先将容器的当前状态保存起来,对应于图论,当我们进行深度优先遍历时,如果遇到了环路,或者当前路径已经遍历结束,就必须要从当前节点返回其父节点,saveContainerState() 把当前的容器状态存入栈中,以便恢复后进行新的遍历。


接下来,我们将容器i 中的水倒入容器j中(通过调用pollWater,然后判断一下,倒完水后,是否达到成功状态(isSuccessed),如果某个容器中含有2升水,那么堆栈中就保留了倒水过程中的一系列的容器状态,通过printStack 把它们打印出来就可以得知倒水过程。pollWater 如果返回true, 表明从容器i倒水到容器j 是成功的, 如果容器i中没有水,那么倒水就不会成功,pollWater 就会返回false, 如果倒水成功,我们递归调用dsfPollingWater 进行下一轮的倒水尝试,其也对应于深度优先搜索。


containerStateExisted 用于判断当前容器的状态是否已经存在过,如果存在,表明搜索进入了环路,必须返回,不然就是死循环,由于所有遍历过的容器状态都保存在堆栈中,因此只要判断堆栈中有没有于当前状态相同的节点就可以了。

运行上面的程序,可以获得以下结果:

--ok--

(10[empty:10, water:0],7[empty:0, water:7],4[empty:0, water:4])->

(10[empty:3, water:7],7[empty:7, water:0],4[empty:0, water:4])->

(10[empty:0, water:10],7[empty:7, water:0],4[empty:3, water:1])->

(10[empty:7, water:3],7[empty:0, water:7],4[empty:3, water:1])->

(10[empty:7, water:3],7[empty:3, water:4],4[empty:0, water:4])->

(10[empty:3, water:7],7[empty:3, water:4],4[empty:4, water:0])->

(10[empty:6, water:4],7[empty:0, water:7],4[empty:4, water:0])->

(10[empty:0, water:10],7[empty:6, water:1],4[empty:4, water:0])->

(10[empty:4, water:6],7[empty:6, water:1],4[empty:0, water:4])->

(10[empty:4, water:6],7[empty:2, water:5],4[empty:4, water:0])->

(10[empty:8, water:2],7[empty:2, water:5],4[empty:0, water:4])->

(10[empty:8, water:2],7[empty:0, water:7],4[empty:2, water:2])


也就是说,通过上面所标示的十二次倒水操作,我们可以从给定的初始状态实现,达到7升或4升容器装有2升水的结果,从程序运行结果看,4升容器中装有2升水。


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值