讲这道题纯粹就是比较好玩,就记录一下.泊松分酒是很著名的一道题,讲的是假设某人有12品脱的啤酒一瓶,想从中倒出六品脱,但是恰巧身边没有6品脱的容器,仅有一个8品脱和一个5品脱的容器,怎样倒才能将啤酒分为两个6品脱呢?
代码:
import java.util.LinkedList;
import java.util.Set;
public class Oil {
static class Status{
static int[] full={12,8,5};//满的状态
int[] bottle=new int[3];//瓶子的状态
Status from;//从哪个状态来的
public Status(int a,int b,int c){
bottle[0]=a;
bottle[1]=b;
bottle[2]=c;
}
//获取某种状态开始下一步的所有的状态
public Set opreation(){
Set res=new HashSet();
//开始倒酒
for(int i=0;i<bottle.length;i++){
for(int j=0;j<bottle.length;j++){
if(i==j) continue; //不倒自己
if(bottle[i]==0) continue;//自己是空的 不倒
if(bottle[j]==full[j]) continue;//对方是满的 不倒
Status t=new Status(bottle[0], bottle[1], bottle[2]);
t.from=this;//从自己这个状态开始变化
//真的开始倒酒了 t.bottle[j]+=t.bottle[i];
t.bottle[i]=0;
if(t.bottle[j]>full[j]){//装不下了
t.bottle[i]=t.bottle[j]-full[j];//满的倒回去
t.bottle[j]=full[j];
}
res.add(t);
}
}
return res;
}
//是否含有某种状态
public boolean has2(int x){
int index=0;
if (bottle[0]==x) index++;
if (bottle[1]==x) index++;
if (bottle[2]==x) index++;
return index==2?true:false;
}
public Status getFrom() {
return from;
}
public String toString(){
return "<" + bottle[0] + "," + bottle[1] + "," + bottle[2] + ">";
}
public int hashCode() {
return 100;
}
public boolean equals(Object obj) {
Status x=(Status)obj;
return bottle[0]==x.bottle[0]&&bottle[1]==x.bottle[1]&&bottle[2]==x.bottle[2];
}
}
public static void main(String[] args) {
Set<Status> all=new HashSet<Status>();//存放所有结果状态
all.add(new Status(12, 0, 0));
for(;;){
Set newset=new HashSet();
for(Status x:all){//所有上一种状态产生所有下一种状态
Set t = x.opreation();
newset.addAll(t);
}
if(all.containsAll(newset)) break;//出口
all.addAll(newset);
}
LinkedList<Status> list=new LinkedList<Status>();//存放有6的一溜
for(Status k:all){
if(k.has2(6)){
while(k!=null){
list.push(k);
k=k.getFrom();//从终止状态开始往上追溯
}
}
}
//输出
while(!list.isEmpty()){
System.out.println(list.pop());
}
}
}
这个解法找到的其实是最优解,至于为什么呢,其实利用set的方法十分巧妙,结果集set里随着一次次的分酒一次次地扩增,当第一次出现含有两个6的状态的时候,再往前追溯,步骤是最少的!因为这个我们想要的状态是第一次出现.
假如我们每次都打印出all集合,可以知道,当第一次找到含有两个6状态的时候程序并没有结束,因为还没有找到所有的状态.
而后面的状态再进行分酒时,仍有可能产生两个6的状态,但是想要加入set集合的时候就行不通了,所以此程序只输出最早加入的那一个解,并且是最优的.
当然这种算法并不能输出所有的解,如果要得到所有的解,我们可以采用以下算法,这种算法借鉴了图的深度搜索(DFS)以及回溯的技巧,需要注意的是,和8皇后问题一样,需要回溯的时机有两个,出错的时候和找到某一组解的时候.
代码:
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Oil {
int[] full = new int[3]; //满状态 容量
int[] bottle = new int[3]; //瓶子的状态
int target = 0; //目标
List<int[]> res = new ArrayList<int[]>();//存放结果
public void opreation(int[] bottle) {
for(int i=0;i<3;i++) {
for(int j=1;j<3;j++){//每个瓶子都不往自己倒 总共6种可能性
int[] temp = bottle.clone();//每次循环都创建临时数组
int to=(i+j)%3;//(i+j)%3 是除每种i瓶子外其他两个瓶子的序号,即要倒的目标
if(temp[i]==0) continue;//自己是空的 不倒
if(temp[to]==full[to]) continue;//对方是满的 不倒
//开始倒酒
temp[to]+=temp[i];
temp[i]=0;
if(temp[to]>full[to]){//装不下了
temp[i]=temp[to]-full[to];//满出来的部分倒回去
temp[to]=full[to];
}
if(had(temp)) continue;//检测是否已经存在相同状态,防止重复
res.add(temp);//添加到结果链表
if(has2(temp)) return;//如果找到有两个想要的状态的结果就返回
opreation(temp);//继续下一次分酒
res.remove(res.size()-1); //回溯 仔细体会
}
}
}
//是否以及含有状态
private boolean had(int[] bottlex) {
for(int[] e:res)
if(e[0]==bottlex[0]&&e[1]==bottlex[1]&&e[2]==bottlex[2]) return true;
return false;
}
//检测找到结果
private boolean has2(int[] bottle) {
int index=0;
for(int i=0;i<bottle.length;i++)
if(bottle[i]==target) index++;
if(index==2){
show(res);//输出
res.remove(res.size()-1);//回溯
return true;
}
return false;
}
//打印
private void show(List<int[]> res) {
for(int[] e:res) {
System.out.println(e[0] + "," + e[1] + "," + e[2]);
}
System.out.println();
}
public static void main(String[] args) {
Oil o = new Oil();
Scanner scanner = new Scanner(System.in);
String s ="";
if(scanner.hasNext()) {
s = scanner.nextLine();
}
String[] data = s.split(",");
int[] d = new int[data.length];
for(int i=0;i<data.length;i++){
d[i] = Integer.parseInt(data[i]);
}
o.full = new int[]{d[0],d[1],d[2]};
o.bottle = new int[]{d[3],d[4],d[5]};
o.target = d[6];
o.res.add(new int[]{d[3],d[4],d[5]});//添加初始状态
o.opreation(o.bottle);
}
}
显然,按照深度搜索并不能有效地找到最优解.上面两种算法都是比较巧的,我也比较喜欢.
如果要同时找到所有解和最优解,用图的广度搜索(BFS)会很方便,这也是网上采用的最多的,代码到处都有,就不写了.