问题描述
迷宫是一个矩形区域,它有一个入口和一个出口,入口位于迷宫的左上角,出口位于迷宫的右下角,在迷宫的内部包含不能穿越的墙或者障碍物。迷宫路径搜索问题就是寻找一条从入口到出口的路径。
基本思路
从迷宫的入口出发,沿着某一个方向(如正东)顺时针对当前位置相邻的东、南、西、北4个位置依次进行判断,搜索可通行的位置。如果有,移动到这个新的相邻的位置上,如果新位置是迷宫的出口,那么就已经找到了一条路径,搜索工作结束,否则从这个新位置开始继续搜索通往路口的路径;若当前位置四周均无通路(四周被障碍物和已经走过的路包围),则将当前位置从路径中删除出去,顺着当前位置的上一个位置的下一个方向继续走,直至到达出口(找到一条通路)或者退出到入口(迷宫没有出口)时结束。
设计思路
1、迷宫数据结构的设计
设迷宫为m行n列矩阵,利用maze[m][n]来表示一个迷宫,“maze[i][j]=0或1;”。其中,0表示不同;1表示通路。当从某点向下试探时,中间点由4个方向(东、南、西、北)可以试探,而迷宫的4个角点只有两个方向,其他的边缘点有3个方向。为使问题简单化用maze[m+2][n+2]来表示迷宫,设迷宫的四周的值全是0。这样做问题就简单多了,每个点的试探方向全是4个。
2、路径试探方向的数据设计
在迷宫矩阵,每个点有4个方向去试探,用x表示行,y表示列,那么与当前点(m,n)的相邻4个点的坐标都可以根据相邻方向得到。为了简化问题,方便的求出新点的坐标,将向4个方向试探造成的坐标增量放在一个结构数组move[4]中,在move数组中,每个元素有两个域组成:横坐标增加量x和纵坐标增加量y。在整个路径试探中,优先从东开始试探,再依次东、南、西、北。
3、栈路径中数据元素的设计
用栈保存搜索的路径,路径中的每个数据元素不仅是顺序到达各点的坐标,还要记录从该点前进的方向,即每走一步,栈中记录的内容为(行、列、前进的方向)。关于前进的方向的表示,将从正东开始沿顺时针进行的东、南、西、北4个方向用0,1,2,3表示。
4、防止重复到达某点
当到达某点(i,j)后使数组maze[i][j]置-1,以便区别未到达过的点,以防止重复试探某位置的目的。
代码实现
1、定义栈中数据元素
栈中数据元素用Point表示,该类拥有x、y、d三个成员变量,依次表示该点所在的行和列,以及来的方向。
public class Point{
public int x,y,d;//
public Point(int x ,int y){
this.x=x;
this.y=y;
}
public Point(int x ,int y,int d){
this.x=x;
this.y=y;
this.d=d;
}
}
2、实现探索迷宫路径
这里使用的是SeqStack,是上一篇文章自己写的一个关于堆栈的顺序栈的实现。也同样可以使用上一篇文章的链栈来实现,或者直接使用java.util.Stack来实现,只需要修改一下sta的生成方式,其他程序部分都是一样的。
public class Migong {
int[][] maze;//迷宮矩陣
int row,col;//迷宮矩陣的行和列
IStack<Point> sta;//存放路徑的棧
//定义位置增量数组
Point[] move = { new Point(0, 1),
new Point(1, 0),
new Point(0, -1),
new Point(-1, 0) };
public Migong(int[][] map) {
row = map.length+2;
col = map[0].length+2;
sta= new SeqStack<Point>(Point.class,row*col);
maze = new int[row][col];
for (int i = 1; i <row-1; i++)
for (int j = 1; j < col-1; j++)
maze[i][j] = map[i - 1][j - 1];
}
public boolean findpath(){
Point temp=null ;
int x, y, d, i, j ;
temp=new Point(1,1,-1); //起点
sta.push(temp); //起点进栈
while(!sta.empty()) { //迷宫路径是否存在的判断条件
temp=sta.pop(); //当前位置没有通路,就将该位置出栈
x=temp.x ;
y=temp.y ;
d=temp.d+1 ; //d是该位置的来的方向,由于是pop出来的,证明了d这个方向是错的,转下一个方向,故+1
while(d<4){ //四个方向都是错的,出此循环,到大循环中pop出去,退回前一个点
i=x+move[d].x ; j=y+move[d].y ; //转过方向之后的新坐标
if( maze[i][j]==1 ) { //该点可以到达
temp=new Point(x, y, d) ;
sta.push(temp); //将该点进栈
System.out.print("("+x+","+y+","+d+") ");
x=i ;
y=j ;
maze[x][y]= -1 ; //做标记,标示该位置已经走过
if (x==row-2&&y==col-2) //到达迷宫出口
{
temp=new Point(x, y, -1) ;
sta.push(temp);
return true ; /*迷宫有路*/
}
else //没有到达迷宫出口,重置d=0,下一个循环找下一个新位置,每个新位置都是从d=0开始试探的,不行再d=1/2/3
d=0 ;
}
else d++ ; //该点不能到达,转下一个方向
}
}
return false;/*迷宫無路*/
}
public Point[] getpath(){
Point[] points=new Point[sta.size()];
for(int i=points.length-1;i>=0;i--){
points[i]=sta.pop();
}
return points;
}
public static void main(String[] args) {
int[][] map = {
{ 1, 1, 0, 1, 1, 1, 0, 1 },
{ 1, 1, 0, 1, 1, 1, 0, 1 },
{ 1, 1, 1, 1, 0, 0, 1, 1 },
{ 1, 0, 0, 0, 1, 1, 1, 1 },
{ 1, 1, 1, 0, 1, 1, 1, 1 },
{ 1, 0, 1, 1, 1, 0, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 0, 1 },
{ 0, 1, 1, 1, 1, 1, 1, 1 }};
int row=map.length,col=map[0].length;
System.out.println("迷宫矩阵:");
for(int i=0;i<row;i++){
for(int j=0;j<col;j++)
{
System.out.print(map[i][j]+" ");
}
System.out.println();
}
Migong mi = new Migong(map);
if (mi.findpath()) {
Point[] points=mi.getpath();
System.out.println("可到达路径:");
for(int i=0;i<points.length;i++){
System.out.print("("+points[i].x+","+points[i].y+","+points[i].d+") ");
}
} else {
System.out.println("没有可到达路径!");
}
}
}
这个算法里面的findpath()可能看起来有点费力,最好从if(maze[i][j]==1)这一行开始分析会比较轻松一点。同时,正是由于堆栈后进先出的性质使这个算法具有了深度优先的特点。如果在探索问题的解时走进了死胡同,则需要退回来从另一条路继续探索,这种思想称为回溯(Backtrack)。
回溯法的基本思想是:对一个包括有很多结点,每个结点有若干个搜索分支的问题,把原问题分解为对若干个子问题求解的算法。当搜索到某个结点、发现无法再继续搜索下去时,就让搜索过程回溯(即退回)到该结点的前一结点,继续搜索这个结点的其他尚未搜索过的分支;如果发现这个结点也无法再继续搜索下去时,就让搜索过程回溯到这个结点的前一结点继续这样的搜索过程;这样的搜索过程一直进行到搜索到问题的解或搜索完了全部可搜索分支没有解存在为止。
而深度优先,在之后数据结构的图论中会详细地涉及到。