关于广度优先搜索:解密迷宫问题三种高效算法Java实现让你轻松穿越未知迷宫

42次阅读

共计 6286 个字符,预计需要花费 16 分钟才能阅读完成。

问题背景

迷宫问题是一个经典的算法问题,指标是找到从迷宫的终点到起点的 最短门路 ,在程序中能够简略的形象成一个 M * N 的二维数组矩阵,而后咱们须要从这个二维矩阵中找到 从终点到起点的最短门路。其中,通常应用 0 示意可行走的路,用 1 示意障碍物,终点和起点别离标记为 S 和 E。例如,下图是一个简略的迷宫问题:

0 0 0 0 0 0
0 1 0 1 1 0
0 1 0 0 0 0
0 0 1 1 0 0
0 1 0 1 0 0
S 0 0 0 E 0

在这个迷宫中,数字 0 示意可行走的路,数字 1 示意障碍物,S 示意终点,E 示意起点。

利用场景

迷宫问题在现实生活中有很多理论利用例子:

  1. 机器人导航:在机器人导航中,机器人须要依据传感器获取的信息来布局门路,从终点到起点。这个过程能够应用迷宫问题的算法来实现,如应用 A* 算法来找到最短门路。
  2. 游戏设计:迷宫问题能够利用于各种类型的游戏中,如谜题解决游戏和角色扮演游戏。在这些游戏中,玩家须要找到一条从终点到起点的门路,同时防止遇到障碍物或危险。
  3. 主动驾驶:在主动驾驶汽车中,汽车须要遵循交通规则、防止障碍物并找到最短门路。这也能够应用迷宫问题的算法来实现,如应用 A* 算法来找到最短门路。
  4. 网络路由:网络路由器须要在各种网络拓扑中寻找最佳门路,以确保数据包在网络中传输时尽可能疾速和牢靠。这也能够应用迷宫问题的算法来实现,如应用 A* 算法来找到最短门路。
  5. 地图利用:在地图利用中,用户须要依据终点和起点寻找最佳门路。这能够应用迷宫问题的算法来实现,如应用 A* 算法来找到最短门路。

罕用算法

求解迷宫问题的算法有多种,其中最常见的是 深度优先搜寻(DFS)算法 广度优先搜寻(BFS)算法 A* 搜索算法。本文将别离介绍这两种算法的实现形式及其优缺点。

深度优先搜寻(DFS)算法

深度优先搜寻(DFS)是一种基于栈或递归的搜索算法,从终点开始,一直地往深处遍历,直到找到起点或无奈持续往下搜寻。在迷宫问题中,DFS 会先选取一个方向往前走,直到无奈后退为止,而后返回上一个节点,尝试其余方向。

DFS 的核心思想是回溯,即在走到绝路时,返回上一个节点,从而摸索其余方向。具体实现上,能够应用递归函数或栈来保护待拜访的节点。

import java.util.*;

public class MazeSolver {
    // 迷宫的行数和列数
    static final int ROW = 5;
    static final int COL = 5;

    // 迷宫的地图,0 示意能够通过的路,1 示意墙壁,2 示意曾经走过的路
    static int[][] map = new int[][]{{0, 1, 1, 1, 1},
        {0, 0, 0, 1, 1},
        {1, 1, 0, 0, 1},
        {1, 1, 1, 0, 1},
        {1, 1, 1, 0, 0}
    };

    // 迷宫的终点和起点
    static final int startX = 0;
    static final int startY = 0;
    static final int endX = 4;
    static final int endY = 4;

    // 存储搜寻门路
    static List<int[]> path = new ArrayList<>();

    // DFS 搜寻迷宫
    public static void dfs(int x, int y) {
        // 如果以后地位是起点,则搜寻实现
        if (x == endX && y == endY) {
            // 打印搜寻门路
            for (int[] p : path) {System.out.print("(" + p[0] + "," + p[1] + ")");
            }
            System.out.println("(" + x + "," + y + ")");
            return;
        }

        // 标记以后地位曾经走过
        map[x][y] = 2;

        // 将以后地位退出搜寻门路
        path.add(new int[]{x, y});

        // 别离搜寻以后地位的上下左右四个方向
        if (x > 0 && map[x-1][y] == 0) {dfs(x-1, y);
        }
        if (y > 0 && map[x][y-1] == 0) {dfs(x, y-1);
        }
        if (x < ROW-1 && map[x+1][y] == 0) {dfs(x+1, y);
        }
        if (y < COL-1 && map[x][y+1] == 0) {dfs(x, y+1);
        }

        // 如果没有找到起点,将以后地位从搜寻门路中移除
        path.remove(path.size()-1);
    }

    public static void main(String[] args) {dfs(startX, startY);
    }
}

深度优先搜寻(DFS)的长处:

  1. 实现简略,不须要额定的数据结构。
  2. 对于有解的迷宫问题,深度优先搜寻可能保障找到一条门路,且门路长度可能会比广度优先搜寻短。
  3. 在空间较大的状况下,深度优先搜寻能够占用更少的内存,因为它只须要保护以后门路上的节点,而不须要保护所有已拜访过的节点。

深度优先搜寻的毛病:

  1. 搜寻的门路可能会非常复杂,可能会陷入死循环或长时间不停的搜寻。
  2. 对于无解的迷宫问题,深度优先搜寻可能会有限地搜寻上来,直到栈溢出或程序解体。
  3. 当要求找到最短门路时,深度优先搜寻不能保障肯定能找到最短门路,因为它是基于回溯的思维,可能会跳过一些更短的门路。
  4. 当搜寻树的深度很大时,深度优先搜寻可能会导致栈溢出的问题。

广度优先搜寻(BFS)

广度优先搜寻(BFS)算法是一种奢侈的搜索算法,它从终点开始逐渐扩大搜寻范畴,直到找到指标节点为止。在搜寻过程中,BFS 会先拜访终点四周的所有节点,再拜访这些节点四周的所有节点,以此类推。因而,BFS 能够保障找到的门路是最短的,但它的工夫复杂度可能很高,尤其是在搜寻空间较大时。

上面是一个基于 BFS 算法的示例代码,用于在一个图中搜寻从终点到指标节点的最短门路:

import java.util.*;

public class MazeSolver {public static void main(String[] args) {

        // 定义迷宫
        int[][] maze = {{0, 1, 0, 0, 0},
            {0, 1, 0, 1, 0},
            {0, 0, 0, 0, 0},
            {0, 1, 1, 1, 0},
            {0, 0, 0, 1, 0}
        };

        // 寻找门路
        List<int[]> path = solve(maze, new int[]{0, 0}, new int[]{4, 2});

        // 输入门路
        if (path != null) {for (int[] point : path) {System.out.println(Arrays.toString(point));
            }
        } else {System.out.println("No solution found.");
        }
    }

    public static List<int[]> solve(int[][] maze, int[] start, int[] end) {

        // 定义宽度优先搜寻所需的队列
        Queue<int[]> queue = new LinkedList<>();
        queue.add(start);

        // 定义门路跟踪数组
        Map<int[], int[]> trace = new HashMap<>();
        trace.put(start, null);

        // 定义曾经拜访过的点汇合
        Set<int[]> visited = new HashSet<>();
        visited.add(start);

        // 定义方向数组,别离示意上下左右四个方向
        int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

        // 开始搜寻
        while (!queue.isEmpty()) {

            // 取出队列中的下一个点
            int[] current = queue.poll();

            // 如果以后点是起点,返回门路
            if (Arrays.equals(current, end)) {List<int[]> path = new ArrayList<>();
                while (current != null) {path.add(current);
                    current = trace.get(current);
                }
                Collections.reverse(path);
                return path;
            }

            // 遍历四个方向
            for (int[] direction : directions) {int[] neighbor = new int[]{current[0] + direction[0], current[1] + direction[1]};

                // 如果街坊在迷宫范畴内,且没有被拜访过,且不是墙,退出队列和拜访汇合,并记录门路
                if (neighbor[0] >= 0 && neighbor[0] < maze.length &&
                    neighbor[1] >= 0 && neighbor[1] < maze[0].length &&
                    !visited.contains(neighbor) && maze[neighbor[0]][neighbor[1]] == 0) {queue.add(neighbor);
                    visited.add(neighbor);
                    trace.put(neighbor, current);
                }
            }
        }

        // 如果搜寻完结还没有找到门路,返回 null
        return null;
    }
}

广度优先搜寻(BFS)的长处:

  1. 找到的第一条门路肯定是最短的,因为 BFS 是依照层级逐个搜寻的,一旦搜寻到指标状态,那么就能够保障这是最短门路。
  2. 能够搜寻出所有可行的门路,而不是仅仅找到一条门路。这对于一些须要获取所有解的问题十分有用。
  3. 在搜寻树比拟小的状况下,BFS 的搜寻速度十分快。

广度优先搜寻(BFS)的毛病:

  1. 空间占用比拟大。在搜寻过程中,须要将所有曾经扩大出的状态都存储在内存中,所以 BFS 须要较多的内存空间,尤其是在搜寻树比拟大的状况下。
  2. 在搜寻树比拟大的状况下,BFS 的工夫复杂度很高。当搜寻树十分大时,BFS 须要搜寻大量的状态,因而工夫复杂度会十分高。
  3. 不能解决有限状态空间问题,即状态空间无限大的问题,例如无限大的图。

A* 搜索算法

A 搜索算法是一种启发式搜索算法,它在广度优先搜寻的根底上引入了启发函数,以更疾速、更精确地搜寻最短门路。启发函数能够评估每个搜寻节点到指标节点的预计间隔,从而优化搜寻方向。具体实现时,能够用一个优先队列来保留搜寻节点,并依照优先级顺次取出每个节点进行搜寻。其中,优先级的计算形式为 f(n) = g(n) + h(n),其中 g(n) 示意从终点到节点 n 的理论间隔,h(n) 示意从节点 n 到起点的预计间隔。应用启发函数的优化可能大幅缩小搜寻工夫。

上面是一个基于 A* 算法的示例代码


import java.util.*;

public class AStar {public static int[] solve(int[][] maze, int[] start, int[] end) {
        int n = maze.length;
        int m = maze[0].length;

        // 将终点退出 openSet 汇合中
        PriorityQueue<int[]> openSet = new PriorityQueue<>((a, b) -> (a[2] + a[3]) - (b[2] + b[3]));
        openSet.offer(new int[]{start[0], start[1], 0, estimateDistance(start, end)});

        // 记录每个点是否曾经被拜访过
        Set<Integer> visited = new HashSet<>();
        visited.add(start[0] * m + start[1]);

        while (!openSet.isEmpty()) {
            // 取出 f 值最小的点
            int[] cur = openSet.poll();
            int x = cur[0];
            int y = cur[1];

            // 如果该点是起点,则返回门路
            if (x == end[0] && y == end[1]) {return new int[]{cur[2], cur[3]};
            }

            // 将该点的所有街坊退出 openSet 中
            int[][] neighbors = new int[][]{{x - 1, y}, {x + 1, y}, {x, y - 1}, {x, y + 1}};
            for (int[] neighbor : neighbors) {int nx = neighbor[0];
                int ny = neighbor[1];

                // 判断街坊是否越界或者是障碍物
                if (nx < 0 || nx >= n || ny < 0 || ny >= m || maze[nx][ny] == 1) {continue;}

                // 如果街坊曾经被拜访过,则跳过
                int code = nx * m + ny;
                if (visited.contains(code)) {continue;}

                // 计算街坊的 g 值和 h 值
                int g = cur[2] + 1;
                int h = estimateDistance(neighbor, end);

                // 将街坊退出 openSet 中
                openSet.offer(new int[]{nx, ny, g, h});
                visited.add(code);
            }
        }

        // 如果 openSet 汇合为空,则阐明不存在可行门路
        return new int[]{-1, -1};
    }

    // 计算估价函数值(曼哈顿间隔)private static int estimateDistance(int[] start, int[] end) {return Math.abs(start[0] - end[0]) + Math.abs(start[1] - end[1]);
    }

    public static void main(String[] args) {
        // 定义迷宫
        int[][] maze = {{0, 1, 0, 0, 0},
                {0, 1, 0, 1, 0},
                {0, 0, 0, 0, 0},
                {0, 1, 1, 1, 0},
                {0, 0, 0, 1, 0}
        };

        // 寻找门路
        int[] path = solve(maze, new int[]{0, 0}, new int[]{4, 2});

        // 输入门路
        if (path != null) {System.out.println(Arrays.toString(path));
        } else {System.out.println("No solution found.");
        }
        solve(maze, maze[1], maze[3]);
    }
}

A* 算法的长处:

  1. A* 算法综合思考了启发式函数和理论代价,因而搜寻效率比拟高。
  2. A* 算法能够找到最短门路,并且可能保障找到的第一条门路肯定是最优门路。

A* 算法的毛病:

  1. 启发式函数的抉择十分要害,不同的启发式函数会导致不同的搜寻后果。如果启发式函数不够精确,那么搜寻后果可能不是最优的。
  2. A算法须要存储 OPEN 表和 CLOSED 表,占用的内存比拟大。如果状态空间比拟大,那么 A 算法的效率会变得非常低。
  3. A* 算法的实现比较复杂,须要对每个状态进行估价和排序,因而算法的实现难度比拟大。

总之,A* 算法是一种十分实用的搜索算法,在门路布局、游戏 AI 等畛域失去广泛应用。在理论利用中,咱们须要依据具体问题的特点抉择适合的启发式函数,并且须要思考算法的内存占用和搜寻效率。

总结

咱们总结一下,在迷宫问题中,深度优先搜寻(DFS)、广度优先搜寻(BFS)和 A* 都能够用来寻找最短门路或最优解。

DFS 实用于以下状况:

  • 空间要求低,不须要保留整个搜寻树,只须要保留以后门路;
  • 所有解的门路长度差异不大,或者只须要找到其中一个解;
  • 迷宫比拟大,而且有很多绝路,采纳 DFS 能够疾速摸索大面积空间。

BFS 实用于以下状况:

  • 须要找到最短门路或最优解;
  • 迷宫中大部分门路长度差异不大;
  • 能够接受较大的空间复杂度,须要保留整个搜寻树。

A* 算法实用于以下状况:

  • 须要找到最短门路或最优解;
  • 须要思考迷宫中的障碍物,即寻找一条避开障碍物的门路;
  • 迷宫比拟大,然而大多数门路都很长,采纳 BFS 不事实;
  • 启发函数选取切当的话,搜寻效率很高。

总体来说,DFS 适宜摸索大面积空间,BFS 适宜寻找最短门路,A* 算法综合了 BFS 和启发式搜寻的长处,更适宜寻找最短门路且迷宫中有障碍物的状况。

正文完
 0