在一些面试算法或智力题中,时不时会遇到容器倒水的问题,例如,有三个容器,分别是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; } }
<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升水。