zoukankan      html  css  js  c++  java
  • 迷宫问题的求解(回溯法、深度优先遍历、广度优先遍历)

    一、问题介绍

      有一个迷宫地图,有一些可达的位置,也有一些不可达的位置(障碍、墙壁、边界)。从一个位置到下一个位置只能通过向上(或者向右、或者向下、或者向左)走一步来实现,从起点出发,如何找到一条到达终点的通路。本文将用两种不同的解决思路,四种具体实现来求解迷宫问题。

      用二维矩阵来模拟迷宫地图,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("");
                }
            }
        }

    测试结果:

      广度优先遍历找出的路径和上面三种方式的结果不一样,它找出的是最短路径。这种方式是从根节点出发,一层一层的往外遍历,当发现某一层上有终点节点时,遍历结束,此时找到的也一定是最短路径,它和二叉树的层序遍历有异曲同工之妙。

      本文个人编写,水平有限,如有错误,恳请指出,欢迎评论分享

  • 相关阅读:
    移动端 video播放器层级过高,遮挡住其他内容
    页面多个H5播放器video,如何点击当前视频其他视频暂停
    h5页面在移动端禁止缩放,双击放大,双指放大
    uni-app发h5本地浏览index.html空白的问题
    运行nvue 页面报错reportJSException >>>> exception function:GraphicActionAddElement, exception:You are trying to add a u-text to a u-text, which is illegal as u-text is not a container
    nvue页面注意事项
    移动端可能遇到的问题解决方案
    将博客搬至CSDN
    安装node后配置在全局配置
    swiper使用loop后,无法获取索引的问题
  • 原文地址:https://www.cnblogs.com/wanghang-learning/p/9430672.html
Copyright © 2011-2022 走看看