为什么要写一个推箱子呢?源于以前喜欢玩这个游戏,可是到了一定关数的时候就太难了,可能几天也想不出来,那时候就想如果有个软件能够在给出问题后,自动生成步骤来解决这个问题多好啊,但最终都没有动手去解决。这次恰好刚看完java书,趁热打铁。
1、看到推箱子游戏很自然的就联想到用2维数组来构造数据结构,分别用数值0、1、4、5来代表空地、工人、箱子、墙,直观、方便,然而在深入思考后发现,2维数组所占空间太大,而且在解决游戏时,我们只关心工人如何走,箱子如何动,对墙和空地毫不关心,二维数组的大部分内容是无用的,有了这部分思考,我们来构建我们需要的数据。
2、创建Point类,在俄罗斯方块中我们用的Point类是java.awt.Point,感觉不是特别好用,这次自己写一个
public class Point implements Comparable<Point>{
public int x;
public int y;
public Point(){};
public Point(int tmpx,int tmpy){
this.x=tmpx;
this.y=tmpy;
}
public Point(Point p){
this.x=p.x;
this.y=p.y;
}
为了方便,这里的x,y均设为public,同时复写equals,toString,hashCode,实现compareTo方法
2、创建BoxWorker类,此类保存工人移动后工人和箱子的位置,如果工人、箱子位置完全相同,则认为此图已遍历过。
public class BoxWorker {
private boolean flag; //记录工人移动到此位置时箱子是否移动,主要为moveback时确定箱子是否移动
private Point pworker;//工人位置
private List<Point> lspbox;//箱子位置
public BoxWorker(Point p,List<Point> lsp){
this.pworker=new Point();
this.lspbox=new ArrayList<Point>();
this.flag=false;
this.add(p, lsp);
}
public BoxWorker(Point p,List<Point> lsp,boolean f){
this(p,lsp);
this.flag=f;
}
public Point getPWorker(){
return this.pworker;
}
public List<Point> getLsPBox(){
return this.lspbox;
}
public boolean getBoxMove(){
return this.flag;
}
public void add(Point p,List<Point> lsp){
this.pworker.x=p.x;
this.pworker.y=p.y;
this.lspbox.clear();
Iterator<Point> iterlsp=lsp.iterator();
while(iterlsp.hasNext()){
Point plsp=iterlsp.next();
this.lspbox.add(new Point(plsp.x,plsp.y));
}
}
然后复写了equals,toString,hashCode
3、到了这里需要的数据格式已经创建完了,我们来创建一个解决问题的类PushBox。
public class PushBox {
int[][] arrtu;//图
Point pworker;//工人
List<Point> lspbox;//箱子位置
List<Point> lspdes;//目标位置
List<BoxWorker> lsbwchecked;//已遍历过的图
Stack<BoxWorker> stkbw;//深度优先算法栈
Stack<Point> stkp;//深度优先走过的路
Queue<BoxWorker> quebw;//宽度优先算法队列}
这个类我只捡主要的方法说,首先要有一个判断是否找到答案的方法,此方法只被内部搜索函数调用,所以定义为私有
private boolean isFind(List<Point> ls){
//箱子是否达到目的地,达到返回true
if(ls.isEmpty()){
return false;
}
if(this.lspdes.isEmpty()){
return false;
}
Iterator<Point> iterpbox=ls.iterator();
while(iterpbox.hasNext()){
if(!this.lspdes.contains(iterpbox.next())){
return false;
}
}
return true;
}
为了搜索更加快捷,我们需要一个函数来判断是否继续沿此方向继续搜索
private boolean isEnd(List<Point> ls){
this.setArrBox(ls,4);
//箱子不可移动并且不在目标点中则结束
Iterator<Point> iterp=ls.iterator();
while(iterp.hasNext()){
Point p=iterp.next();
if(isDead(p) && !this.lspdes.contains(p)){
this.setArrBox(ls,0);
return true;
}
}
this.setArrBox(ls,0);
return false;
}
还需要一个判断工人能否移动的函数
private boolean canMove(Point pw,List<Point> ls,int x,int y){}这里没有贴代码,总共3个判断1、是否越界;2、移动方向连续两个物体;3、此图是否已经遍历过
前期的各种判断都写完了,那就开始推箱子吧~_~
private boolean move(Point pw,List<Point> ls,int x,int y){//第一个参数是工人坐标,第2个参数是箱子坐标,第3、4个参数是移动方向
为了记录工人移动时,箱子是否移动,特意加了返回值
这里为了避免大量重复代码,用一个move代替了4个方向上的moveup等,吸取了俄罗斯方块上的经验
//向前移动
boolean flag=false;//箱子是否移动
//修改lspbox
Iterator<Point> iterbox=ls.iterator();
while(iterbox.hasNext()){
Point p=iterbox.next();
if(p.x==pw.x+x && p.y==pw.y+y){
p.x=p.x+x;
p.y=p.y+y;
flag=true;
}
}
//修改pworker,stkresult,lsbwchecked
这个stkresult好像最后没啥用,所以在成员变量那里没有写
stkresult.push((new Point(pw.x+x,pw.y+y)));
pw.x=pw.x+x;
pw.y=pw.y+y;
// System.out.println(pw+"#"+ls);
lsbwchecked.add(new BoxWorker(pw,ls,flag));
//level++;
return flag;
}
有向前移动就得有向后退,要不然就真得撞南墙了
private BoxWorker moveBack(Point pw,List<Point> ls,int x,int y,boolean flag){
//向后移动
//修改lspbox
if(flag){
Iterator<Point> iterbox=ls.iterator();
while(iterbox.hasNext()){
Point p=iterbox.next();
if(p.x==pw.x+x && p.y==pw.y+y){
p.x=p.x-x;
p.y=p.y-y;
}
}
}
//修改pworker,stkresult
//stkresult.pop();
//System.out.print(p+";");
pw.x=pw.x-x;
pw.y=pw.y-y;
//lsbwchecked.add(new BoxWorker(this.pworker,this.lspbox));
//level--;
return new BoxWorker(pw,ls);
}
这下就真的该开始搜索了,关于搜索的函数,我写了三个,一个找最短路径的,两个是一条路走到黑-----深度搜索,用递归的那个深度搜索运行了几次没成功,后来没有继续研究了,这里放一个找最短路径的函数
public int searchLFS(){
boolean flag;
this.quebw.offer(new BoxWorker(this.pworker,this.lspbox));
this.stkp.push(new Point(this.pworker.x,this.pworker.y));
while(!this.quebw.isEmpty()){
BoxWorker bw=this.quebw.poll();
Point pw=bw.getPWorker();
List<Point> lsp=bw.getLsPBox();
if(canMove(pw,lsp,-1,0)){
flag=move(pw,lsp,-1,0);//把能移动的图加入到队列
stkp.push(new Point(-1,0));
if(this.isFind(lsp)){
System.out.println(bw);
return 2;
}
if(!this.isEnd(lsp)){
this.quebw.offer(new BoxWorker(pw,lsp,flag));
}
moveBack(pw,lsp,-1,0,flag);
}
if(canMove(pw,lsp,1,0)){
flag=move(pw,lsp,1,0);
stkp.push(new Point(1,0));
if(this.isFind(lsp)){
System.out.println(bw);
return 2;
}
if(!this.isEnd(lsp)){
this.quebw.offer(new BoxWorker(pw,lsp,flag));
}
moveBack(pw,lsp,1,0,flag);
}
if(canMove(pw,lsp,0,-1)){
flag=move(pw,lsp,0,-1);
stkp.push(new Point(0,-1));
if(this.isFind(lsp)){
System.out.println(bw);
return 2;
}
if(!this.isEnd(lsp)){
this.quebw.offer(new BoxWorker(pw,lsp,flag));
}
moveBack(pw,lsp,0,-1,flag);
}
if(canMove(pw,lsp,0,1)){
flag=move(pw,lsp,0,1);
stkp.push(new Point(0,1));
if(this.isFind(lsp)){
System.out.println(bw);
return 2;
}
if(!this.isEnd(lsp)){
this.quebw.offer(new BoxWorker(pw,lsp,flag));
}
moveBack(pw,lsp,0,1,flag);
}
}
return 0;
}
做到这里推箱子解决问题的方案是找到了,但别急,还没有输出呢,有方案但看不到,和没有不是一样么,这时候才发现自己的BoxWorker类应该有一个把方案串起来的数据,没经验学费总要交的,可是现在又不想该基础的东西了,能有其他办法吗?答案是肯定的,因为我们有一个存储工人走过的图的集合lsbwchecked,可以用它来进行输出,虽然麻烦点,但总是有个输出了
public void printlsbw(){}定义了一个函数把结果输出。
到了这里,问题已经解决了,但思考以上程序,可优化的地方还很多,比如:每次判断是否结束当前搜索时isEnd函数中是对所有箱子进行判断,而实际上只需要对当前移动了的箱子进行判断即可;再比如可以把4个方向的移动建一个数组,那样在search中用一个循环就能解决问题,减少代码行数,提高代码可读性;这里就不一一列举了,与人共勉吧。