每日算法 — 使用java实现迷宫生成与求解:DFS生成与A*求解

一、游戏介绍与问题建模

迷宫问题是计算机科学中最经典的算法练习之一,它不仅有趣,而且涵盖了图论、搜索算法、数据结构等多个核心知识点。从古希腊神话中的米诺斯迷宫,到现代游戏中的随机地图生成,迷宫始终是一个引人入胜的话题。在算法领域,迷宫的生成与求解分别对应着图的生成树构造和最短路径搜索两大经典问题。

1.1 迷宫的定义与分类

从数学角度来看,一个二维迷宫可以被定义为:在一个 m×n 的网格中,每个格子之间由墙壁隔开,通过移除部分墙壁形成通道,使得存在至少一条从起点到终点的路径。

根据不同的特性,迷宫可以分为多种类型:

类型 特点 典型算法
完美迷宫(Perfect Maze) 任意两点之间有且仅有一条路径,无环、无孤立区域 DFS递归回溯、Prim、Kruskal
不完美迷宫 存在环路或多条路径,增加了解的多样性 随机删除墙壁法
braid迷宫 无死胡同,每个点至少有两个方向可选 在完美迷宫基础上打通死胡同

本文主要讨论完美迷宫的生成,这是最基础也是最具数学美感的一类迷宫。从图论的角度看,完美迷宫本质上就是网格图的一棵生成树——所有顶点连通且边数恰好为顶点数减一,自然不存在环路。

1.2 问题建模

将迷宫问题抽象为图论模型:

  • 顶点(Vertex):每个网格单元格是一个顶点
  • 边(Edge):两个相邻单元格之间如果没有墙壁,则存在一条边
  • 迷宫生成:从完全图(所有墙壁都存在)出发,逐步移除墙壁,最终得到一棵生成树
  • 迷宫求解:在生成的图中,找到从起点到终点的最短路径

这种建模方式非常巧妙——迷宫生成对应”加边”(移除墙壁=添加边),迷宫求解对应”寻路”,二者统一在图论框架下,展现了算法之美。

1.3 本文算法概览

本文将实现三种核心算法,覆盖迷宫问题的两大维度:

  1. DFS递归回溯生成法:最经典的迷宫生成算法,生成的迷宫走廊长、转弯少,视觉效果优美
  2. A*寻路算法:带启发函数的广度优先搜索,是求解迷宫最短路径的最优选择
  3. 并查集随机生成法:基于Kruskal最小生成树思想,生成的迷宫更均匀、更”开阔”

二、状态表示与编码

高效的数据结构是算法性能的基础。对于迷宫问题,我们需要一种既能清晰表达墙壁状态,又便于算法操作的编码方式。

2.1 单元格墙壁编码

每个单元格有四面墙(上、右、下、左),我们用一个 4 位二进制数来表示墙壁状态,每一位代表一面墙是否存在(1表示有墙,0表示无墙)。

位编号:  3   2   1   0
墙壁:   上   右  下   左

使用位运算的好处是:墙壁的打通和恢复都可以通过简单的位操作完成,时间复杂度为 O(1)。

/**
 * 迷宫单元格墙壁编码常量
 * 使用4位二进制表示四面墙的状态:1表示有墙,0表示无墙
 * 位3=上墙,位2=右墙,位1=下墙,位0=左墙
 */
public class MazeConstants {
    public static final int WALL_TOP = 8;    // 1000 上墙
    public static final int WALL_RIGHT = 4;  // 0100 右墙
    public static final int WALL_BOTTOM = 2; // 0010 下墙
    public static final int WALL_LEFT = 1;   // 0001 左墙
    public static final int ALL_WALLS = 15;  // 1111 四面墙都有

    // 获取对面墙壁的编码
    public static int oppositeWall(int wall) {
        switch (wall) {
            case WALL_TOP: return WALL_BOTTOM;
            case WALL_BOTTOM: return WALL_TOP;
            case WALL_LEFT: return WALL_RIGHT;
            case WALL_RIGHT: return WALL_LEFT;
            default: return 0;
        }
    }

    // 根据方向获取偏移量 [行偏移, 列偏移]
    public static int[] directionOffset(int wall) {
        switch (wall) {
            case WALL_TOP: return new int[]{-1, 0};
            case WALL_BOTTOM: return new int[]{1, 0};
            case WALL_LEFT: return new int[]{0, -1};
            case WALL_RIGHT: return new int[]{0, 1};
            default: return new int[]{0, 0};
        }
    }
}

2.2 迷宫网格数据结构

使用二维数组存储迷宫,每个元素是一个整数,表示该单元格的墙壁状态。初始状态下所有单元格四面都是墙。

import java.util.*;

/**
 * 迷宫类:封装迷宫数据结构和基本操作
 */
public class Maze {
    private final int rows;       // 迷宫行数
    private final int cols;       // 迷宫列数
    private final int[][] grid;   // 网格数据,每个格子存储墙壁编码
    private final Random random;

    // 四个方向的墙壁编码,用于随机遍历
    private static final int[] WALLS = {
        MazeConstants.WALL_TOP,
        MazeConstants.WALL_RIGHT,
        MazeConstants.WALL_BOTTOM,
        MazeConstants.WALL_LEFT
    };

    public Maze(int rows, int cols) {
        this.rows = rows;
        this.cols = cols;
        this.grid = new int[rows][cols];
        this.random = new Random();
        // 初始化:所有格子四面都是墙
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                grid[i][j] = MazeConstants.ALL_WALLS;
            }
        }
    }

    /**
     * 打通 (row, col) 单元格的指定墙壁
     * 同时打通相邻单元格的对应墙壁
     */
    public void removeWall(int row, int col, int wall) {
        // 移除当前格子的墙
        grid[row][col] &= ~wall;
        // 移除相邻格子的对墙
        int[] offset = MazeConstants.directionOffset(wall);
        int newRow = row + offset[0];
        int newCol = col + offset[1];
        if (isValidCell(newRow, newCol)) {
            grid[newRow][newCol] &= ~MazeConstants.oppositeWall(wall);
        }
    }

    /**
     * 检查两个相邻单元格之间是否有墙
     */
    public boolean hasWall(int row, int col, int wall) {
        return (grid[row][col] & wall) != 0;
    }

    /**
     * 判断坐标是否在迷宫范围内
     */
    public boolean isValidCell(int row, int col) {
        return row >= 0 && row < rows && col >= 0 && col < cols;
    }

    // 获取打乱顺序的方向数组,用于随机化
    public int[] getShuffledWalls() {
        int[] shuffled = Arrays.copyOf(WALLS, WALLS.length);
        for (int i = shuffled.length - 1; i > 0; i--) {
            int j = random.nextInt(i + 1);
            int temp = shuffled[i];
            shuffled[i] = shuffled[j];
            shuffled[j] = temp;
        }
        return shuffled;
    }

    // Getter方法
    public int getRows() { return rows; }
    public int getCols() { return cols; }
    public int[][] getGrid() { return grid; }
    public Random getRandom() { return random; }
}

三、DFS递归回溯生成算法

深度优先搜索(DFS)递归回溯法是最经典的迷宫生成算法,其思想简单而优雅:从任意单元格出发,随机选择一个未访问的邻居,打通墙壁并递归访问,直到所有单元格都被访问。

3.1 算法原理

DFS生成迷宫的核心步骤:

  1. 选择起始单元格,标记为已访问
  2. 随机打乱四个方向的顺序
  3. 依次检查每个方向:
  4. 如果邻居在边界内且未被访问
  5. 打通当前单元格与邻居之间的墙壁
  6. 递归访问邻居
  7. 当所有方向都尝试完毕,回溯到上一层

这个过程本质上是在进行一次”随机深度优先遍历”,遍历过程中走过的边就构成了迷宫的通道。由于DFS的特性,生成的迷宫往往具有长长的走廊较少的分支,视觉上非常有特色。

3.2 完整代码实现

/**
 * DFS递归回溯迷宫生成器
 */
public class DFSMazeGenerator {

    /**
     * 使用DFS递归回溯算法生成完美迷宫
     */
    public static void generate(Maze maze) {
        int rows = maze.getRows();
        int cols = maze.getCols();
        boolean[][] visited = new boolean[rows][cols];
        // 从左上角开始生成
        dfs(maze, 0, 0, visited);
    }

    /**
     * 深度优先搜索递归函数
     * @param maze 迷宫对象
     * @param row 当前行
     * @param col 当前列
     * @param visited 访问标记数组
     */
    private static void dfs(Maze maze, int row, int col, boolean[][] visited) {
        visited[row][col] = true;

        // 随机打乱四个方向的顺序,保证迷宫的随机性
        int[] shuffledWalls = maze.getShuffledWalls();

        // 尝试每个方向
        for (int wall : shuffledWalls) {
            int[] offset = MazeConstants.directionOffset(wall);
            int newRow = row + offset[0];
            int newCol = col + offset[1];

            // 检查邻居是否合法且未访问
            if (maze.isValidCell(newRow, newCol) && !visited[newRow][newCol]) {
                // 打通墙壁
                maze.removeWall(row, col, wall);
                // 递归访问邻居
                dfs(maze, newRow, newCol, visited);
            }
        }
    }

    /**
     * 非递归版本(使用显式栈),避免大迷宫时栈溢出
     */
    public static void generateIterative(Maze maze) {
        int rows = maze.getRows();
        int cols = maze.getCols();
        boolean[][] visited = new boolean[rows][cols];

        // 使用栈存储待处理的单元格
        Deque<int[]> stack = new ArrayDeque<>();
        stack.push(new int[]{0, 0});
        visited[0][0] = true;

        while (!stack.isEmpty()) {
            int[] current = stack.peek();
            int row = current[0];
            int col = current[1];

            // 找到所有未访问的邻居
            List<int[]> unvisitedNeighbors = new ArrayList<>();
            int[] shuffledWalls = maze.getShuffledWalls();

            for (int wall : shuffledWalls) {
                int[] offset = MazeConstants.directionOffset(wall);
                int newRow = row + offset[0];
                int newCol = col + offset[1];
                if (maze.isValidCell(newRow, newCol) && !visited[newRow][newCol]) {
                    unvisitedNeighbors.add(new int[]{newRow, newCol, wall});
                }
            }

            if (!unvisitedNeighbors.isEmpty()) {
                // 随机选择一个未访问的邻居
                int[] next = unvisitedNeighbors.get(maze.getRandom().nextInt(unvisitedNeighbors.size()));
                int newRow = next[0];
                int newCol = next[1];
                int wall = next[2];

                // 打通墙壁
                maze.removeWall(row, col, wall);
                visited[newRow][newCol] = true;
                stack.push(new int[]{newRow, newCol});
            } else {
                // 没有未访问的邻居,回溯
                stack.pop();
            }
        }
    }
}

3.3 算法特性分析

DFS递归回溯法生成的迷宫具有以下特点:

  • 完美迷宫保证:每个单元格恰好被访问一次,且通过墙壁打通保证连通性,因此必然是完美迷宫
  • 长走廊特性:由于深度优先的特性,算法倾向于”一条路走到黑”,生成的迷宫走廊较长
  • 随机性良好:通过打乱方向顺序,每次生成的迷宫都不同
  • 实现简单:代码量少,逻辑清晰,是理解迷宫生成的最佳入门算法

时间复杂度:O(rows × cols),每个单元格恰好被访问一次,每个方向检查是常数时间。

空间复杂度:O(rows × cols),需要visited数组和递归栈(或显式栈),最坏情况下栈深度为rows×cols。


四、A*寻路算法求解

生成迷宫之后,自然的问题就是:如何找到从起点到终点的最短路径?A*算法是解决这个问题的最佳选择,它结合了Dijkstra算法的正确性和贪心算法的高效性。

4.1 算法原理

A*算法的核心是估价函数:

f(n) = g(n) + h(n)

其中:
g(n):从起点到节点 n 的实际代价(已走步数)
h(n):从节点 n 到终点的估计代价(启发函数)
f(n):节点 n 的总估价,决定搜索优先级

启发函数 h(n) 的选择至关重要:
– 如果 h(n) = 0,A* 退化为 Dijkstra 算法,保证最短路径但搜索范围大
– 如果 h(n) ≤ 实际距离,A* 保证找到最短路径(可采纳性)
– 如果 h(n) = 实际距离,A* 直接走最优路径,效率最高
– 如果 h(n) > 实际距离,A* 可能找不到最短路径,但速度更快

对于网格迷宫,曼哈顿距离是最常用的启发函数:

h(x, y) = |x - endX| + |y - endY|

由于只能沿上下左右移动,曼哈顿距离恰好等于最短路径的下界,满足可采纳性条件。

4.2 完整代码实现

/**
 * A*寻路算法求解迷宫
 */
public class AStarSolver {

    /**
     * 搜索节点类
     */
    static class Node implements Comparable<Node> {
        int row;        // 行坐标
        int col;        // 列坐标
        int g;          // 从起点到当前节点的实际代价
        int h;          // 启发估计代价
        int f;          // f = g + h
        Node parent;    // 父节点,用于回溯路径

        Node(int row, int col, int g, int h, Node parent) {
            this.row = row;
            this.col = col;
            this.g = g;
            this.h = h;
            this.f = g + h;
            this.parent = parent;
        }

        // 按f值升序排列,f值小的优先出队
        @Override
        public int compareTo(Node other) {
            return Integer.compare(this.f, other.f);
        }
    }

    /**
     * 使用A*算法求解迷宫最短路径
     * @param maze 迷宫对象
     * @param startRow 起点行
     * @param startCol 起点列
     * @param endRow 终点行
     * @param endCol 终点列
     * @return 路径坐标列表,从起点到终点;无解返回null
     */
    public static List<int[]> solve(Maze maze, int startRow, int startCol, 
                                     int endRow, int endCol) {
        int rows = maze.getRows();
        int cols = maze.getCols();

        // 优先队列(最小堆):f值最小的节点优先扩展
        PriorityQueue<Node> openSet = new PriorityQueue<>();
        // 已访问集合:记录已找到最优路径的节点
        boolean[][] closed = new boolean[rows][cols];
        // 记录已在开放集中的节点的最小g值,避免重复加入
        int[][] gScore = new int[rows][cols];
        for (int i = 0; i < rows; i++) {
            Arrays.fill(gScore[i], Integer.MAX_VALUE);
        }

        // 起点入队
        int startH = manhattanDistance(startRow, startCol, endRow, endCol);
        Node startNode = new Node(startRow, startCol, 0, startH, null);
        openSet.offer(startNode);
        gScore[startRow][startCol] = 0;

        // 四个方向
        int[] walls = {
            MazeConstants.WALL_TOP,
            MazeConstants.WALL_RIGHT,
            MazeConstants.WALL_BOTTOM,
            MazeConstants.WALL_LEFT
        };

        while (!openSet.isEmpty()) {
            Node current = openSet.poll();

            // 到达终点,回溯构造路径
            if (current.row == endRow && current.col == endCol) {
                return reconstructPath(current);
            }

            // 如果已经在关闭集中,跳过(可能有多个相同节点在开放集)
            if (closed[current.row][current.col]) {
                continue;
            }
            closed[current.row][current.col] = true;

            // 扩展四个方向的邻居
            for (int wall : walls) {
                // 检查当前方向是否有墙
                if (maze.hasWall(current.row, current.col, wall)) {
                    continue;
                }

                int[] offset = MazeConstants.directionOffset(wall);
                int newRow = current.row + offset[0];
                int newCol = current.col + offset[1];

                // 边界检查(理论上有墙就不会越界,这里做双重保险)
                if (!maze.isValidCell(newRow, newCol)) {
                    continue;
                }

                // 如果已关闭,跳过
                if (closed[newRow][newCol]) {
                    continue;
                }

                // 计算新的g值
                int tentativeG = current.g + 1;

                // 如果新路径更优,加入开放集
                if (tentativeG < gScore[newRow][newCol]) {
                    gScore[newRow][newCol] = tentativeG;
                    int h = manhattanDistance(newRow, newCol, endRow, endCol);
                    Node neighbor = new Node(newRow, newCol, tentativeG, h, current);
                    openSet.offer(neighbor);
                }
            }
        }

        // 开放集为空仍未找到终点,说明无解(完美迷宫不会出现)
        return null;
    }

    /**
     * 曼哈顿距离启发函数
     */
    private static int manhattanDistance(int x1, int y1, int x2, int y2) {
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }

    /**
     * 从终点回溯到起点,构造完整路径
     */
    private static List<int[]> reconstructPath(Node endNode) {
        List<int[]> path = new ArrayList<>();
        Node current = endNode;
        while (current != null) {
            path.add(new int[]{current.row, current.col});
            current = current.parent;
        }
        // 反转,使路径从起点到终点
        Collections.reverse(path);
        return path;
    }
}

4.3 算法性能分析

A*算法的性能高度依赖启发函数的质量:

  • 最优性保证:当启发函数是可采纳的(h ≤ 实际距离),A* 一定能找到最短路径
  • 时间复杂度:最坏情况 O(N log N),其中 N 为节点数。实际运行中,好的启发函数能大幅减少搜索节点数
  • 空间复杂度:O(N),开放集和关闭集都需要存储节点信息

与其他寻路算法的对比:

算法 最短路径保证 性能 适用场景
BFS 慢(搜索所有等距节点) 无权图、小迷宫
DFS 快但可能绕远 只求连通性
Dijkstra 中等 带权图、无启发信息
A* ✅(可采纳时) 最快 有良好启发函数的场景

五、并查集随机生成法

并查集(Union-Find)生成法,又称Kruskal算法生成法,是另一种经典的迷宫生成算法。它基于最小生成树的Kruskal算法思想,通过随机选择墙壁并打通来生成迷宫。

5.1 算法原理

并查集生成法的核心思想:

  1. 初始状态:每个单元格都是独立的集合(四面都是墙)
  2. 收集所有内部墙壁,随机打乱顺序
  3. 按随机顺序依次考虑每面墙:
  4. 如果墙两侧的单元格属于不同集合
  5. 打通这面墙,合并两个集合
  6. 当所有单元格都属于同一个集合时,算法结束

这个过程本质上是Kruskal最小生成树算法的变体——只不过我们不是按边权从小到大选边,而是随机选边,最终生成的是一棵随机生成树

与DFS生成的迷宫相比,并查集法生成的迷宫:
– 通道更短、分支更多
– 视觉上更”均匀”、更”开阔”
– 死胡同相对较少

5.2 完整代码实现

/**
 * 并查集(Union-Find)数据结构
 */
class UnionFind {
    private int[] parent;  // 父节点数组
    private int[] rank;    // 秩(树的高度上界),用于按秩合并
    private int count;     // 连通分量数量

    public UnionFind(int size) {
        parent = new int[size];
        rank = new int[size];
        count = size;
        for (int i = 0; i < size; i++) {
            parent[i] = i;  // 每个元素初始是自己的父节点
            rank[i] = 0;
        }
    }

    /**
     * 查找根节点(带路径压缩)
     */
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // 路径压缩:递归时直接指向根
        }
        return parent[x];
    }

    /**
     * 合并两个集合(按秩合并)
     * @return true表示执行了合并,false表示已经在同一集合
     */
    public boolean union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX == rootY) {
            return false;  // 已经在同一集合,无需合并
        }

        // 按秩合并:将秩小的树挂到秩大的树下
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank[rootX]++;  // 秩相同时,新根秩加1
        }

        count--;
        return true;
    }

    public int getCount() {
        return count;
    }
}

/**
 * 并查集迷宫生成器(Kruskal算法变体)
 */
public class UnionFindMazeGenerator {

    /**
     * 使用并查集(Kruskal)算法生成完美迷宫
     */
    public static void generate(Maze maze) {
        int rows = maze.getRows();
        int cols = maze.getCols();
        int totalCells = rows * cols;

        // 初始化并查集,每个格子一个集合
        UnionFind uf = new UnionFind(totalCells);

        // 收集所有内部墙壁
        // 墙壁用 (cellIndex, wallType) 表示
        List<int[]> walls = new ArrayList<>();

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                int cellIndex = i * cols + j;
                // 只添加右墙和下墙,避免重复(左墙=左边格子的右墙,上墙=上面格子的下墙)
                if (j < cols - 1) {
                    walls.add(new int[]{cellIndex, MazeConstants.WALL_RIGHT});
                }
                if (i < rows - 1) {
                    walls.add(new int[]{cellIndex, MazeConstants.WALL_BOTTOM});
                }
            }
        }

        // 随机打乱墙壁顺序
        Collections.shuffle(walls, maze.getRandom());

        // 依次处理每面墙
        for (int[] wallInfo : walls) {
            // 如果所有格子已经连通,可以提前结束
            if (uf.getCount() == 1) {
                break;
            }

            int cellIndex = wallInfo[0];
            int wallType = wallInfo[1];
            int row = cellIndex / cols;
            int col = cellIndex % cols;

            // 获取相邻格子的索引
            int[] offset = MazeConstants.directionOffset(wallType);
            int neighborRow = row + offset[0];
            int neighborCol = col + offset[1];
            int neighborIndex = neighborRow * cols + neighborCol;

            // 如果两个格子不在同一集合,打通墙壁并合并
            if (uf.union(cellIndex, neighborIndex)) {
                maze.removeWall(row, col, wallType);
            }
        }
    }
}

5.3 算法特性分析

并查集生成法的优势:

  • 均匀性好:生成的迷宫各向同性,没有DFS那种明显的长走廊偏向
  • 并查集高效:路径压缩+按秩合并后,每次操作接近O(1)
  • 直观易懂:算法过程像”随机拆墙”,非常符合直觉

时间复杂度:O(rows × cols × α(rows×cols)),其中α是阿克曼函数的反函数,增长极慢,几乎可以视为常数。整体接近线性时间。

空间复杂度:O(rows × cols),需要存储所有墙壁和并查集数组。


六、复杂度分析与可视化扩展

6.1 算法复杂度总结

算法 时间复杂度 空间复杂度 迷宫特点
DFS递归回溯 O(N) O(N) 长走廊、转弯少、分支少
并查集(Kruskal) O(N α(N)) ≈ O(N) O(N) 均匀分布、分支多、更”开阔”
A*寻路 O(N log N) 最坏 O(N) 保证最短路径、启发函数决定效率

注:N = rows × cols,即迷宫单元格总数。

6.2 可视化输出

为了直观展示迷宫效果,我们添加一个ASCII可视化方法,方便在控制台打印迷宫:

/**
 * 迷宫可视化工具类
 */
public class MazeVisualizer {

    /**
     * 将迷宫以ASCII字符形式打印
     * 可选参数:路径高亮显示
     */
    public static String toAscii(Maze maze, List<int[]> path) {
        int rows = maze.getRows();
        int cols = maze.getCols();
        int[][] grid = maze.getGrid();

        // 路径标记集合
        Set<String> pathSet = new HashSet<>();
        if (path != null) {
            for (int[] p : path) {
                pathSet.add(p[0] + "," + p[1]);
            }
        }

        StringBuilder sb = new StringBuilder();

        // 打印顶部边框
        for (int j = 0; j < cols; j++) {
            sb.append("+---");
        }
        sb.append("+\n");

        // 打印每一行
        for (int i = 0; i < rows; i++) {
            // 打印左侧竖线开头
            sb.append("|");

            // 打印单元格内容和右墙
            for (int j = 0; j < cols; j++) {
                // 路径高亮
                if (pathSet.contains(i + "," + j)) {
                    sb.append(" * ");
                } else {
                    sb.append("   ");
                }
                // 右墙
                if ((grid[i][j] & MazeConstants.WALL_RIGHT) != 0) {
                    sb.append("|");
                } else {
                    sb.append(" ");
                }
            }
            sb.append("\n");

            // 打印下墙
            for (int j = 0; j < cols; j++) {
                sb.append("+");
                if ((grid[i][j] & MazeConstants.WALL_BOTTOM) != 0) {
                    sb.append("---");
                } else {
                    sb.append("   ");
                }
            }
            sb.append("+\n");
        }

        return sb.toString();
    }

    /**
     * 主函数:演示迷宫生成与求解
     */
    public static void main(String[] args) {
        int rows = 10;
        int cols = 15;

        // 1. 使用DFS生成迷宫
        Maze dfsMaze = new Maze(rows, cols);
        DFSMazeGenerator.generate(dfsMaze);
        System.out.println("=== DFS生成的迷宫 ===");
        System.out.println(toAscii(dfsMaze, null));

        // 2. A*求解
        List<int[]> path = AStarSolver.solve(dfsMaze, 0, 0, rows-1, cols-1);
        System.out.println("=== A*求解结果(路径长度: " + path.size() + "步)===");
        System.out.println(toAscii(dfsMaze, path));

        // 3. 使用并查集生成迷宫
        Maze ufMaze = new Maze(rows, cols);
        UnionFindMazeGenerator.generate(ufMaze);
        System.out.println("=== 并查集生成的迷宫 ===");
        System.out.println(toAscii(ufMaze, null));
    }
}

6.3 扩展方向

掌握了基础算法后,还可以向以下方向深入探索:

  1. 更多生成算法:Prim算法、Wilson算法(均匀随机生成树)、Eller算法(行式生成,空间O(n))
  2. 三维迷宫:将二维网格扩展到三维,增加上下方向的通道
  3. 多种寻路算法对比:BFS、DFS、Dijkstra、双向BFS、IDA*等
  4. 迷宫难度评估:如何量化一个迷宫的”难度”?路径长度?死胡同数量?
  5. GUI可视化:使用Java Swing或JavaFX实现动画展示生成过程
  6. 游戏化应用:在迷宫中加入怪物、道具、机关,变成完整的小游戏

6.4 写在最后

迷宫问题是算法学习的绝佳载体——它从一个简单有趣的问题出发,串联起了图论、搜索、数据结构等多个核心知识点。DFS让我们体会到递归与回溯的优雅,A*让我们见识到启发式搜索的威力,并查集让我们感受到数据结构的精妙。

更重要的是,迷宫问题展示了一个深刻的道理:看似复杂的问题,往往可以用简洁的算法优雅地解决。希望这篇文章能让你不仅学会了迷宫算法,更体会到算法之美。