一、問題介紹
有一個迷宮地圖,有一些可達的位置,也有一些不可達的位置(障礙、牆壁、邊界)。從一個位置到下一個位置只能通過向上(或者向右、或者向下、或者向左)走一步來實現,從起點出發,如何找到一條到達終點的通路。本文將用兩種不同的解決思路,四種具體實現來求解迷宮問題。
用二維矩陣來模擬迷宮地圖,1代表該位置不可達,0代表該位置可達。每走過一個位置就將地圖的對應位置標記,以免重復。找到通路后打印每一步的坐標,最終到達終點位置。
封裝了點Dot,以及深度優先遍歷用到的Block,廣度優先遍歷用到的WideBlock。
private int[][] map = { //迷宮地圖,1代表牆壁,0代表通路 {1,1,1,1,1,1,1,1,1,1}, {1,0,0,1,0,0,0,1,0,1}, {1,0,0,1,0,0,0,1,0,1}, {1,0,0,0,0,1,1,0,0,1}, {1,0,1,1,1,0,0,0,0,1}, {1,0,0,0,1,0,0,0,0,1}, {1,0,1,0,0,0,1,0,0,1}, {1,0,1,1,1,0,1,1,0,1}, {1,1,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1,1} }; private int mapX = map.length - 1; //地圖xy邊界 private int mapY = map[0].length - 1; private int startX = 1; //起點 private int startY = 1; private int endX = mapX - 1; //終點 private int endY = mapY - 1;
//內部類,封裝一個點
public class Dot{
private int x; //行標
private int y; //列標
public Dot(int x , int y) {
this.x = x;
this.y = y;
}
public int getX(){
return x;
}
public int getY(){
return y;
}
}
//內部類,封裝走過的每一個點,自帶方向
public class Block extends Dot{
private int dir; //方向,1向上,2向右,3向下,4向左
public Block(int x , int y) {
super(x , y);
dir = 1;
}
public int getDir(){
return dir;
}
public void changeDir(){
dir++;
}
}
/*
廣度優先遍歷用到的數據結構,它需要一個指向父節點的索引
*/
public class WideBlock extends Dot{
private WideBlock parent;
public WideBlock(int x , int y , WideBlock p){
super(x , y);
parent = p;
}
public WideBlock getParent(){
return parent;
}
}
二、回溯法
思路:從每一個位置出發,下一步都有四種選擇(上右下左),先選擇一個方向,如果該方向能夠走下去,那么就往這個方向走,當前位置切換為下一個位置。如果不能走,那么換個方向走,如果所有方向都走不了,那么退出當前位置,到上一步的位置去,當前位置切換為上一步的位置。一致這樣執行下去,如果當前位置是終點,那么結束。如果走過了所有的路徑都沒能到達終點,那么無解。下面看代碼
private Stack<Block> stack = new Stack<Block>(); //回溯法存儲路徑的棧 public void findPath1(){ Block b = new Block(startX,startY); stack.push(b); //起點進棧 while(!stack.empty()){ //棧空代表所有路徑已走完,沒有找到通路 Block t = stack.peek(); int x = t.getX(); //獲取棧頂元素的x int y = t.getY(); //獲取棧頂元素的y int dir = t.getDir(); //獲取棧頂元素的下一步方向 map[x][y] = 1; //把地圖上對應的位置標記為1表示是當前路徑上的位置,防止往回走 if(t.getX() == endX && t.getY() == endY) {//已達終點 return ; } switch(dir){ case 1: //向上走一步 if(x - 1 >= 0 && map[x - 1][y] == 0){ //判斷向上走一步是否出界&&判斷向上走一步的那個位置是否可達 stack.push(new Block(x - 1 , y)); //記錄該位置 } t.changeDir(); //改變方向,當前方向已走過 continue; //進入下一輪循環 case 2: if(y + 1 <= mapY && map[x][y+1] == 0){ stack.push(new Block(x , y + 1)); } t.changeDir(); continue; case 3: if(x + 1 <= mapX && map[x+1][y] == 0){ stack.push(new Block(x + 1 , y)); } t.changeDir(); continue; case 4: if(y - 1 >= 0 && map[x][y - 1] == 0){ stack.push(new Block(x , y - 1)); } t.changeDir(); continue; } t = stack.pop(); //dir > 4 當前Block節點已經沒有方向可走,出棧 map[t.getX()][t.getY()] = 0; //出棧元素對應的位置已經不再當前路徑上,表示可達 } } //打印棧 public void printStack(){ int count = 1;
while(!stack.empty()){
Block b = stack.pop();
System.out.print("(" + b.getX() + "," + b.getY() + ") ");
if(count % 10 == 0)
System.out.println("");
count++;
} }
測試結果:
遞歸實現:
private Stack<Block> s = new Stack<Block>(); //回溯法全局輔助棧 private Stack<Block> stack = new Stack<Block>(); //回溯法存儲路徑的棧
public void findPath2(){ if(startX >= 0 && startX <= mapX && startY >= 0 && startY <= mapY && map[startX][startY] == 0){ find(startX , startY); } } private void find(int x , int y){
map[x][y] = 1; if(x == endX && y == endY) { s.push(new Block(x , y)); while(!s.empty()){ stack.push(s.pop()); } //return ; //在此處返回會使后續遞歸再次尋找路線會經過這里,如果不返回,整個函數執行完畢,所有路徑都會被訪問到 } s.push(new Block(x , y));if( x - 1 >= 0 && map[x - 1][y] == 0 ){ //可以往上走,那么往上走 find(x - 1 , y); } if(x + 1 <= mapX && map[x + 1][y] == 0){ //可以往下走,那么往下走 find(x + 1 , y); } if(y - 1 >= 0 && map[x][y - 1] ==0){ //往左 find(x , y - 1); } if(y + 1 <= mapY && map[x][y + 1] == 0){ find(x , y + 1); } if(!s.empty()){ s.pop(); } }
測試結果:
三、深度優先遍歷
思路:和上面的回溯法思想基本一樣,能向某個方向走下去就一直向那個方向走,不能走就切換方向,所有方向都不能走了就回到上一層位置。
private Stack<Dot> stackDeep = new Stack<Dot>(); //深度遍歷時用的存儲棧 private Stack<Dot> sDeep = new Stack<Dot>(); //深度遍歷時用到的輔助棧 public void findPath3() { // 判斷起點的合法性 if (startX >= 0 && startX <= mapX && startY >= 0 && startY <= mapY && map[startX][startY] == 0) { deepFirst(startX, startY); } } public void deepFirst(int x, int y) { Dot b = new Dot(x, y); sDeep.push(b); map[x][y] = 1; // 標記已訪問 if (x == endX && y == endY) { while (!sDeep.empty()) { stackDeep.push(sDeep.pop()); } //return ; //此處的return和上一個遞歸的return一樣,如果返回 } for (int i = 1; i <= 4; i++) { // 在每個方向上進行嘗試 if (i == 1) { // 上 if (x - 1 >= 0 && map[x - 1][y] == 0) { deepFirst(x - 1, y); } } else if (i == 2) { // 右 if (y + 1 <= mapY && map[x][y + 1] == 0) { deepFirst(x, y + 1); } } else if (i == 3) { // 下 if (x + 1 <= mapX && map[x + 1][y] == 0) { deepFirst(x + 1, y); } } else { // 左 if (y - 1 >= 0 && map[x][y - 1] == 0) { deepFirst(x, y - 1); } } } if(!sDeep.empty()) { sDeep.pop(); // 四個方向都已嘗試過,並且沒成功,退棧 } } //打印深度優先遍歷的棧 public void printDeepStack(){ int count = 1; while(!stackDeep.empty()){ Dot b = stackDeep.pop(); System.out.print("(" + b.getX() + "," + b.getY() + ") "); if(count++ % 10 == 0){ System.out.println(""); } } }
測試結果:
棧實現、遞歸和深度優先遍歷,這三者的執行思路是一樣的,都可以看做二叉樹先根遍歷的變體,起點看做根節點,每個選擇方向看做一個分叉,四個方向對應四個子節點,如果某個節點四個方向都走不了,那么它就是葉子節點。每走到一個葉子節點就是一條完整的執行路徑,只不過不一定到達了終點。按照先根遍歷的方式,最終會走完每一條路徑,如果在路徑中找到了終點,那么記錄下這條路徑線索。二叉樹先根遍歷的遞歸實現對應了上面的遞歸實現,只不過這里是四叉樹,棧實現可以看成先根遍歷的非遞歸算法,而深度優先遍歷和先跟遍歷本就有異曲同工之妙。
四、廣度優先遍歷
思路:這種方法和前面三種是不同的思路。先從根節點出發,將當前節點的所有可達子節點依次訪問,在依次訪問子節點的子節點,一直下去直到所有節點被遍歷。
private WideBlock wb; //廣度遍歷的終點節點 /* 廣度優先遍歷 */ public void findPath4(){ wideFirst(); } public void wideFirst(){ WideBlock start = new WideBlock(startX , startY , null); Queue<WideBlock> q = new LinkedList<WideBlock>(); map[startX][startY] = 1; q.offer(start); while(q.peek() != null){ WideBlock b = q.poll(); int x = b.getX(); int y = b.getY(); if(x == endX && y == endY){ //判斷當前節點是否已達終點 wb = b; return; } if (x - 1 >= 0 && map[x - 1][y] == 0) { //判斷當前節點的能否向上走一步,如果能,則將下一步節點加入隊列 q.offer(new WideBlock(x - 1 , y , b)); //子節點入隊 map[x - 1][y] = 1; //標記已訪問 } if (y + 1 <= mapY && map[x][y + 1] == 0) { //判斷當前節點能否向右走一步 q.offer(new WideBlock(x , y + 1 , b)); map[x][y + 1] = 1; } if (x + 1 <= mapX && map[x + 1][y] == 0) { q.offer(new WideBlock(x + 1 , y , b)); map[x + 1][y] = 1; } if (y - 1 >= 0 && map[x][y - 1] == 0) { q.offer(new WideBlock(x , y - 1 , b)); map[x][y -1] = 1; } } } //打印廣度遍歷的路徑 public void printWidePath(){ WideBlock b = wb; int count = 1; while(b != null){ System.out.print("(" + b.getX() + "," + b.getY() + ") "); b = b.getParent(); if(count++ % 10 == 0){ System.out.println(""); } } }
測試結果:
廣度優先遍歷找出的路徑和上面三種方式的結果不一樣,它找出的是最短路徑。這種方式是從根節點出發,一層一層的往外遍歷,當發現某一層上有終點節點時,遍歷結束,此時找到的也一定是最短路徑,它和二叉樹的層序遍歷有異曲同工之妙。
本文個人編寫,水平有限,如有錯誤,懇請指出,歡迎評論分享