使用for循环绘制一个小迷宫_Java 算法 走出迷宫!

前几天参加字节跳动招聘的笔试,遇到了一个走迷宫的题目(笔试题目,就不挂原图了),当时没有做出来,今天周末,上午总结了一下,来说一说这个迷宫到底怎么走

这篇文章将会分为三个部分,分别是:

  • 深度优先算法:获得一条路径
  • 广度优先算法:获得最短路径的长度
  • 广度优先算法:在有传送门的迷宫中寻找最短路径

一、深度优先算法:获得一条路径

在这个题目中,不涉及传送门,地图可以这样表示:

756302eeaae01f16d353fc761d91536c.png

其中,1 的位置表示了墙,即不可使用,0 的位置则为路,因为现在值要求获得一条路径,是不是最佳路径我们不管,所以我们可以使用“一条路走到黑”的思路,深度优先。

(注:这里假设的启点为左上,重点为右下,在启点与重点非这种情形下,算法仍然适用)。

具体的做法是这样的:

首先我们先定义一个Position类来存储一下当前位置,也可以用数组,这里新建类是方便表示和理解:

class Position{
        int row;
        int cow;
        public Position(int row,int cow){
            this.cow = cow;
            this.row = row;
        }
        public void show(){
            System.out.println(this.row + "    " + this.cow);
        }
    }

这里我还定义了一个show方法,是我在写的时候调试用的,大家可以不管或者直接删除。

然后我们需要设立一个栈,当然也可使用队列来存储这条路径,其次,我们还维护一个访问状态的二维数组,避免在一条圈上反复寻找,造成死循环。

public List<int[]> solutionDFS(int[][] nums){
        int rows = nums.length;
        int cows = nums[0].length;
        int[][] visited = new int[rows][cows];
        Stack<Position> stack = new Stack<>();
        Position p = new Position(0,0);    // 记录一下当前位置,在启点
        stack.add(p);
        visited[0][0] = 1;     //  访问状态设置为 1 ,代表已经访问过了
        Position temp;
        //  只要找到了终点就退出循环
        //  始终没有找到,也会导致栈弹空
        while (!stack.isEmpty()&& !(p.row == rows-1 && p.cow == cows-1)){
            p = stack.peek();     // 获取上一个访问过的位置
           //  按照方向  → ↓  ←  ↑的顺序依次进行试探性的走一步
           //  如果能走通(在迷宫范围内,不是墙,而且没有访问过,就可以认为是可以走)
            if (p.cow+1<cows && nums[p.row][p.cow+1] == 0 && visited[p.row][p.cow+1] != 1){
                temp = new Position(p.row,p.cow+1);
                stack.add(temp);
                visited[temp.row][temp.cow] = 1;
            }else if (p.row+1<rows && nums[p.row+1][p.cow] == 0 && visited[p.row+1][p.cow] != 1){
                temp = new Position(p.row+1,p.cow);
                stack.add(temp);
                visited[temp.row][temp.cow] = 1;
            }else if (p.cow-1>-1 && nums[p.row][p.cow-1] == 0 && visited[p.row][p.cow-1] != 1) {
                temp = new Position(p.row,p.cow-1);
                stack.add(temp);
                visited[temp.row][temp.cow] = 1;
            }else if (p.row-1 >-1 && nums[p.row-1][p.cow] == 0 && visited[p.row-1][p.cow] != 1){
                temp = new Position(p.row-1,p.cow);
                stack.add(temp);
                visited[temp.row][temp.cow] = 1;
            }else {
            // 如果没有尝试了四个方向都没有走通,说明上一个点的选取有问题,直接弹出
                stack.pop();
            }
        }
       // 最后根据还在栈里的的元素,推导出一挑可用路径
        if (stack.isEmpty()) return new LinkedList<>();
        Deque<int[]> deque = new LinkedList<>();
        for (Position po:stack) {
            deque.addLast(new int[]{po.row,po.cow});
        }
        return (List)deque;
    }

这是针对上面的迷宫的一个输出:

bca6d973cfbb68148cf0698b62261fbd.png

路径比较长,我拆成了左右两个部分进行展示。

二、广度优先算法,获得最短路径

如果想要获得一条最短路径,那么我们可以使用广度优先的思路,“一层一层的剥开我的心”

!啊,回来!

广度优先的思路其实也很容易理解,拿到一个点后,根据这个点的步数,更新这个点周围四个方向上的最小步数,直到全局稳定(也就是没有更小值可以更新了)

public int solutionBFS(int[][] nums){
        int rows = nums.length;
        int cows = nums[0].length;
        int[][] count = new int[rows][cows];
    //  首先对计数的数组进行初始化
        for (int i = 0;i<rows;i++){
            Arrays.fill(count[i],Integer.MAX_VALUE);
        }
        count[0][0] = 0;
   //  由于深度优先算法是和遍历的层数有关的,所以我们使用双向链表来操作
   //  前面添加,后面取用(还可以使用两个栈来进行交替使用)
        Deque<Position> deque = new LinkedList<>();
        Position p = new Position(0,0);
        deque.add(p);
        int[] r = {0,1,0,-1};
        int[] c = {1,0,-1,0};
        while (!deque.isEmpty()){
            p = deque.pollLast();
            for (int i = 0; i<4;i++){
                int tempR = p.row+r[i];
                int tempC = p.cow+c[i];
                if (tempR>-1 && tempR<rows && tempC>-1 && tempC<cows && nums[tempR][tempC] == 0){
                    //  如果能够进行更新,那就将这个位置再次压入队列中,等待下一次更新
                    if (count[tempR][tempC] > count[p.row][p.cow]+1){
                        count[tempR][tempC] = count[p.row][p.cow]+1;
                        Position temp = new Position(tempR,tempC);
                        deque.addFirst(temp);
                    }
                }
            }
        }
        return count[rows-1][cows-1];
    }

这是示例迷宫的输出:

80ac2cfd442635f460b7b3a9333939a4.png

由于和上面是一个地图,所以数过之后你会发现,确实最少路径是17步。

三、广度优先算法:在有传送门的迷宫中寻找最短路径

这是我们这次主要要说的迷宫,有传送门的迷宫。

一个示例的带有传送门的地图可能是这样的:

5ffa58fcddaefd5f3003aaaba5d5e121.png

其中:

-2 表示启点,-3表示终点,0表示普通路径,-1表示墙,大于0的数字则表示传送门(能够保证传送门成对出现)。

我的思考过程是这样的:

由于传送门之间的穿送是不记录步数的,直觉的思路是:当遇到一个传送门时,直接传送,进而继续进行广度优先搜索(寻找最短路径)。

我认为这个思路可行,但是实现起来可能比较麻烦,因为:

  • 一方面,你不知道传送门用的顺序
  • 另一方面,你不知道传送门使用的次数,可能是一次,也可能是0次。而广度优先,对一个点的访问极有可能更多次。

下面来说一下我的思路:

仍然是广度优先没有错,但是是两次,另外在两次之间,对传送门的步数取最小值:

public int solutionTransfer(int[][] nums){
        int rows = nums.length;
        int cows = nums[0].length;
        HashMap<Integer,List<int[]>> hashMap = new HashMap<>();
        int endRow=0,endCow=0,startRow=0,startCow = 0;
//        先获得起始位置、终点位置,以及各个传送门的位置
//        将传送门的代号和位置保存到hashmap中
        for (int i = 0; i<rows;i++){
            for (int j = 0;j<cows;j++){
                if (nums[i][j] == -2){
                    startRow = i;
                    startCow = j;
                }else if (nums[i][j] == -3){
                    endRow = i;
                    endCow = j;
                }else {
                    if (nums[i][j]>0){
                        if ( !hashMap.containsKey(nums[i][j])){
                            List<int[]> list = new LinkedList<>();
                            hashMap.put(nums[i][j],list);
                        }
                        hashMap.get(nums[i][j]).add(new int[]{i,j});
                    }
                }
            }
        }
        //  第一步广度优先算法
        int[][] count = new int[rows][cows];
        for (int i = 0;i<rows;i++){
            Arrays.fill(count[i],Integer.MAX_VALUE);
        }
        count[startRow][startCow] = 0;
        Deque<Position> deque = new LinkedList<>();
        Position p = new Position(startRow,startCow);
        deque.add(p);
        int[] r = {0,1,0,-1};
        int[] c = {1,0,-1,0};
        while (!deque.isEmpty()){
            p = deque.pollLast();
            for (int i = 0; i<4;i++){
                int tempR = p.row+r[i];
                int tempC = p.cow+c[i];
                if (tempR>-1 && tempR<rows && tempC>-1 && tempC<cows && nums[tempR][tempC] != -1){
                    if (count[tempR][tempC] > count[p.row][p.cow]+1){
                        count[tempR][tempC] = count[p.row][p.cow]+1;
                        Position temp = new Position(tempR,tempC);
                        deque.addFirst(temp);
                    }
                }
            }
        }
        // 通过hash来获得每一对传送门,嗖嗖嗖~
        for (int targ : hashMap.keySet()){
            List<int[]> list = hashMap.get(targ);
            int[] in = list.get(0);
            int[] out = list.get(1);
            if (count[in[0]][in[1]] < count[out[0]][out[1]]){
                count[out[0]][out[1]] = count[in[0]][in[1]];
            }else {
                count[in[0]][in[1]] = count[out[0]][out[1]];
            }
            // 将更改过步数的路径继续压队列
            // 这里的代码还可以优化,实际上只需要将原来大的那个点压入队列即可
            // 也就是放到 if 和else 里面去,虽然看到了,但是,懒 
            deque.addFirst(new Position(in[0],in[1]));
            deque.addFirst(new Position(out[0],out[1]));
        }
          //  新一轮的广度优先搜索,更新最小步骤数
        while (!deque.isEmpty()){
            p = deque.pollLast();
            for (int i = 0; i<4;i++){
                int tempR = p.row+r[i];
                int tempC = p.cow+c[i];
                if (tempR>-1 && tempR<rows && tempC>-1 && tempC<cows && nums[tempR][tempC] != -1){
                    if (count[tempR][tempC] > count[p.row][p.cow]+1){
                        count[tempR][tempC] = count[p.row][p.cow]+1;
                        Position temp = new Position(tempR,tempC);
                        deque.addFirst(temp);
                    }
                }
            }
        }
        //  返回最小步骤数
        return count[endRow][endCow];
    }

针对给出的示例迷宫(带有传送门),测试结果是这样的:

4fb96296fbbc8e90bfff9f5a7c75a5e8.png

感谢评论区 @华樱 指出的问题,对上面第三个问题做了一点点改动,改动代码如下:

public int solutionTransferS(int[][] nums){
        int rows = nums.length;
        int cows = nums[0].length;
        HashMap<Integer,List<int[]>> hashMap = new HashMap<>();
        int endRow=0,endCow=0,startRow=0,startCow = 0;
//        先获得起始位置、终点位置,以及各个传送门的位置
//        将传送门的代号和位置保存到hashmap中
        for (int i = 0; i<rows;i++){
            for (int j = 0;j<cows;j++){
                if (nums[i][j] == -2){
                    startRow = i;
                    startCow = j;
                }else if (nums[i][j] == -3){
                    endRow = i;
                    endCow = j;
                }else {
                    if (nums[i][j]>0){
                        if ( !hashMap.containsKey(nums[i][j])){
                            List<int[]> list = new LinkedList<>();
                            hashMap.put(nums[i][j],list);
                        }
                        hashMap.get(nums[i][j]).add(new int[]{i,j});
                    }
                }
            }
        }
        int[][] count = new int[rows][cows];
        for (int i = 0;i<rows;i++){
            Arrays.fill(count[i],Integer.MAX_VALUE);
        }
        count[startRow][startCow] = 0;
        Position p = new Position(startRow,startCow);
        Deque<Position> deque = new LinkedList<>();
        deque.addFirst(p);
        List<int[]> list;
        int[] r = {0,1,0,-1};
        int[] c = {1,0,-1,0};
        while (!deque.isEmpty()){
            p = deque.pollLast();
            for (int i = 0; i<4;i++){
                int tempR = p.row+r[i];
                int tempC = p.cow+c[i];
                if (tempR>-1 && tempR<rows && tempC>-1 && tempC<cows && nums[tempR][tempC] != -1){
                    if (nums[p.row][p.cow] >0 && nums[tempR][tempC] == nums[p.row][p.cow]){
                        continue;
                    }
                    if (count[tempR][tempC] > count[p.row][p.cow]+1){
                        count[tempR][tempC] = count[p.row][p.cow]+1;
                        Position temp = new Position(tempR,tempC);
                        deque.addFirst(temp);
                    }
                }
            }
            if (hashMap.containsKey(nums[p.row][p.cow])){
                list = hashMap.get(nums[p.row][p.cow]);
                for (int[] t:list) {
                    if (p.row!=t[0] || p.cow!=t[1]){
                        if (count[t[0]][t[1]] > count[p.row][p.cow]){
                            count[t[0]][t[1]] = count[p.row][p.cow];
                            p.row = t[0];
                            p.cow = t[1];
                            deque.addFirst(p);
                        }
                        break;
                    }
                }
            }
        }
        return count[endRow][endCow];
    }

至于仍然存在的代码冗余问题,确实是还没理解足够透彻,欢迎大家继续挑毛病。

想要获得全部代码,欢迎到我的仓库:

PluteW/InterviewCode​github.com
0be80725a1697a04c1dab3bfb3a170ea.png

本次代码为:BitDance_Zoumigong_200321.java

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值