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