迷宮問題的求解(回溯法、深度優先遍歷、廣度優先遍歷)

1、問題介紹算法

  有一個迷宮地圖,有一些可達的位置,也有一些不可達的位置(障礙、牆壁、邊界)。從一個位置到下一個位置只能經過向上(或者向右、或者向下、或者向左)走一步來實現,從起點出發,如何找到一條到達終點的通路。本文將用兩種不一樣的解決思路,四種具體實現來求解迷宮問題。數據結構

  用二維矩陣來模擬迷宮地圖,1表明該位置不可達,0表明該位置可達。每走過一個位置就將地圖的對應位置標記,以避免重複。找到通路後打印每一步的座標,最終到達終點位置。ide

  封裝了點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;
        }
    }

  

2、回溯法測試

  思路:從每個位置出發,下一步都有四種選擇(上右下左),先選擇一個方向,若是該方向可以走下去,那麼就往這個方向走,當前位置切換爲下一個位置。若是不能走,那麼換個方向走,若是全部方向都走不了,那麼退出當前位置,到上一步的位置去,當前位置切換爲上一步的位置。一致這樣執行下去,若是當前位置是終點,那麼結束。若是走過了全部的路徑都沒能到達終點,那麼無解。下面看代碼this

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++;
        } }

測試結果:spa

 

 遞歸實現:3d

    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(); } }

測試結果:code

3、深度優先遍歷blog

  思路:和上面的回溯法思想基本同樣,能向某個方向走下去就一直向那個方向走,不能走就切換方向,全部方向都不能走了就回到上一層位置。

 

    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("");
            }
        }
    }

 

測試結果:

 

  棧實現、遞歸和深度優先遍歷,這三者的執行思路是同樣的,均可以看作二叉樹先根遍歷的變體,起點看作根節點,每一個選擇方向看作一個分叉,四個方向對應四個子節點,若是某個節點四個方向都走不了,那麼它就是葉子節點。每走到一個葉子節點就是一條完整的執行路徑,只不過不必定到達了終點。按照先根遍歷的方式,最終會走完每一條路徑,若是在路徑中找到了終點,那麼記錄下這條路徑線索。二叉樹先根遍歷的遞歸實現對應了上面的遞歸實現,只不過這裏是四叉樹,棧實現能夠當作先根遍歷的非遞歸算法,而深度優先遍歷和先跟遍歷本就有殊途同歸之妙。

 

4、廣度優先遍歷

  思路:這種方法和前面三種是不一樣的思路。先從根節點出發,將當前節點的全部可達子節點依次訪問,在依次訪問子節點的子節點,一直下去直到全部節點被遍歷。

 

    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("");
            }
        }
    }

 

測試結果:

  廣度優先遍歷找出的路徑和上面三種方式的結果不同,它找出的是最短路徑。這種方式是從根節點出發,一層一層的往外遍歷,當發現某一層上有終點節點時,遍歷結束,此時找到的也必定是最短路徑,它和二叉樹的層序遍歷有殊途同歸之妙。

  本文我的編寫,水平有限,若有錯誤,懇請指出,歡迎評論分享

相關文章
相關標籤/搜索