引言
迷宫生成与求解是算法领域的经典问题,广泛应用于游戏地图设计、机器人路径规划等领域。本文将用Java实现一种优雅的迷宫生成算法——递归分割法(Recursive Division),以及高效的A*寻路算法来求解生成的迷宫。通过这个项目,你将深入理解分治策略在空间生成中的应用,以及启发式搜索在路径规划中的威力。
一、递归分割迷宫生成算法
1.1 算法思想
递归分割法的核心思想是:从一个完全开放的空间开始,递归地添加墙壁将其分割成更小的区域,同时在墙壁上随机开设通道,确保迷宫的连通性。
1.2 算法步骤
- 初始状态:整个区域没有墙壁(全是通路)
- 在当前区域随机选择一行和一列添加十字墙壁
- 在十字墙壁的三边上各开一个通道(保留一边封闭以形成迷宫结构)
- 对形成的四个子区域递归执行步骤2-3
- 当区域足够小时停止分割
二、Java核心实现
import java.util.*;
/**
* 迷宫单元格类型枚举
*/
enum CellType {
WALL('#'), // 墙壁
PATH(' '), // 通路
START('S'), // 起点
END('E'), // 终点
VISITED('.'), // 已访问(用于显示搜索过程)
SOLUTION('*');// 解决方案路径
private final char symbol;
CellType(char symbol) {
this.symbol = symbol;
}
public char getSymbol() {
return symbol;
}
}
/**
* 坐标点类
*/
class Point {
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return x * 31 + y;
}
@Override
public String toString() {
return "(" + x + "," + y + ")";
}
}
/**
* 迷宫类
* 使用递归分割法生成,支持A*寻路求解
*/
public class MagicMaze {
private final int width; // 迷宫宽度(必须是奇数)
private final int height; // 迷宫高度(必须是奇数)
private final CellType[][] grid;
private final Random random;
public MagicMaze(int width, int height) {
// 确保宽度和高度为奇数,以便墙壁和通路交替排列
this.width = width % 2 == 0 ? width + 1 : width;
this.height = height % 2 == 0 ? height + 1 : height;
this.grid = new CellType[this.height][this.width];
this.random = new Random();
initializeGrid();
generateMaze(1, 1, this.width - 2, this.height - 2);
// 设置起点和终点
grid[1][1] = CellType.START;
grid[this.height - 2][this.width - 2] = CellType.END;
}
/**
* 初始化网格:全部设为通路
*/
private void initializeGrid() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 边界设为墙壁
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) {
grid[y][x] = CellType.WALL;
} else {
grid[y][x] = CellType.PATH;
}
}
}
}
/**
* 递归分割生成迷宫
* @param left 左边界
* @param top 上边界
* @param right 右边界
* @param bottom 下边界
*/
private void generateMaze(int left, int top, int right, int bottom) {
int w = right - left + 1;
int h = bottom - top + 1;
// 停止条件:区域太小无法分割
if (w < 3 || h < 3) {
return;
}
// 选择随机的墙壁位置(必须是偶数索引,这样通道在奇数索引)
int wallX = left + 1 + random.nextInt((w - 1) / 2) * 2;
int wallY = top + 1 + random.nextInt((h - 1) / 2) * 2;
// 在选定位置画十字墙壁
drawCrossWall(left, top, right, bottom, wallX, wallY);
// 对四个象限递归分割
// 左上
generateMaze(left, top, wallX - 1, wallY - 1);
// 右上
generateMaze(wallX + 1, top, right, wallY - 1);
// 左下
generateMaze(left, wallY + 1, wallX - 1, bottom);
// 右下
generateMaze(wallX + 1, wallY + 1, right, bottom);
}
/**
* 在指定位置画十字墙壁,并在三边开口
*/
private void drawCrossWall(int left, int top, int right, int bottom, int wallX, int wallY) {
// 确定十字墙壁的四个端点
int xStart = left;
int xEnd = right;
int yStart = top;
int yEnd = bottom;
// 画水平墙壁
for (int x = xStart; x <= xEnd; x++) {
grid[wallY][x] = CellType.WALL;
}
// 画垂直墙壁
for (int y = yStart; y <= yEnd; y++) {
grid[y][wallX] = CellType.WALL;
}
// 在十字的三边上开设通道
// 四个方向:上、下、左、右,选择其中三个开设通道
List<Integer> directions = new ArrayList<>(Arrays.asList(0, 1, 2, 3));
Collections.shuffle(directions, random);
// 开设三个通道,保留一个封闭
for (int i = 0; i < 3; i++) {
int dir = directions.get(i);
createPassage(wallX, wallY, dir, left, top, right, bottom);
}
}
/**
* 在指定方向开设通道
*/
private void createPassage(int wallX, int wallY, int direction,
int left, int top, int right, int bottom) {
int passageX = wallX;
int passageY = wallY;
switch (direction) {
case 0: // 上:在垂直墙壁的上半部分随机开通道
passageY = top + random.nextInt((wallY - top) / 2 + 1) * 2;
break;
case 1: // 下:在垂直墙壁的下半部分随机开通道
passageY = wallY + 1 + random.nextInt((bottom - wallY) / 2 + 1) * 2;
if (passageY > bottom) passageY = bottom;
break;
case 2: // 左:在水平墙壁的左半部分随机开通道
passageX = left + random.nextInt((wallX - left) / 2 + 1) * 2;
break;
case 3: // 右:在水平墙壁的右半部分随机开通道
passageX = wallX + 1 + random.nextInt((right - wallX) / 2 + 1) * 2;
if (passageX > right) passageX = right;
break;
}
grid[passageY][passageX] = CellType.PATH;
}
/**
* 检查坐标是否在迷宫范围内
*/
public boolean isValid(int x, int y) {
return x >= 0 && x < width && y >= 0 && y < height;
}
/**
* 检查坐标是否是通路
*/
public boolean isPassable(int x, int y) {
return isValid(x, y) && grid[y][x] != CellType.WALL;
}
/**
* 获取起点坐标
*/
public Point getStart() {
return new Point(1, 1);
}
/**
* 获取终点坐标
*/
public Point getEnd() {
return new Point(width - 2, height - 2);
}
/**
* 打印迷宫
*/
public void print() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
System.out.print(grid[y][x].getSymbol());
}
System.out.println();
}
}
/**
* 复制网格状态(用于标记搜索路径)
*/
public CellType[][] getGridCopy() {
CellType[][] copy = new CellType[height][width];
for (int y = 0; y < height; y++) {
System.arraycopy(grid[y], 0, copy[y], 0, width);
}
return copy;
}
public int getWidth() { return width; }
public int getHeight() { return height; }
}
三、A*寻路算法实现
import java.util.*;
/**
* A*寻路节点
*/
class AStarNode implements Comparable<AStarNode> {
Point point;
double g; // 从起点到当前节点的实际代价
double h; // 从当前节点到终点的启发式估计代价
AStarNode parent;
public AStarNode(Point point, double g, double h, AStarNode parent) {
this.point = point;
this.g = g;
this.h = h;
this.parent = parent;
}
// f = g + h,总代价
public double getF() {
return g + h;
}
@Override
public int compareTo(AStarNode other) {
return Double.compare(this.getF(), other.getF());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AStarNode)) return false;
AStarNode other = (AStarNode) o;
return this.point.equals(other.point);
}
@Override
public int hashCode() {
return point.hashCode();
}
}
/**
* A*寻路求解器
*/
public class AStarSolver {
/**
* 曼哈顿距离启发式函数
* 仅适用于四方向移动(上、下、左、右)
*/
public static double manhattanDistance(Point a, Point b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
/**
* 欧几里得距离启发式函数
* 适用于八方向移动或对角线移动
*/
public static double euclideanDistance(Point a, Point b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
/**
* A*寻路主算法
* @param maze 迷宫对象
* @param start 起点
* @param end 终点
* @return 从起点到终点的路径,若不可达则返回null
*/
public static List<Point> solve(MagicMaze maze, Point start, Point end) {
// 优先队列(按f值排序)
PriorityQueue<AStarNode> openSet = new PriorityQueue<>();
// 已访问集合
Set<Point> closedSet = new HashSet<>();
// 记录每个点的最优g值,用于快速查找
Map<Point, Double> gScore = new HashMap<>();
AStarNode startNode = new AStarNode(start, 0, manhattanDistance(start, end), null);
openSet.add(startNode);
gScore.put(start, 0.0);
// 四个方向:上、下、左、右
int[][] directions = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
while (!openSet.isEmpty()) {
AStarNode current = openSet.poll();
// 到达终点
if (current.point.equals(end)) {
return reconstructPath(current);
}
closedSet.add(current.point);
// 遍历邻居
for (int[] dir : directions) {
int nx = current.point.x + dir[0];
int ny = current.point.y + dir[1];
Point neighbor = new Point(nx, ny);
// 检查是否可通行且未访问
if (!maze.isPassable(nx, ny) || closedSet.contains(neighbor)) {
continue;
}
// 计算从起点经当前点到邻居的代价
double tentativeG = current.g + 1; // 每步代价为1
// 如果找到更优路径
if (!gScore.containsKey(neighbor) || tentativeG < gScore.get(neighbor)) {
gScore.put(neighbor, tentativeG);
double h = manhattanDistance(neighbor, end);
AStarNode neighborNode = new AStarNode(neighbor, tentativeG, h, current);
// 如果邻居不在开放列表中,添加它
openSet.remove(neighborNode); // 先移除旧节点(如果存在)
openSet.add(neighborNode);
}
}
}
// 开放列表为空但未找到路径,说明不可达
return null;
}
/**
* 回溯重建路径
*/
private static List<Point> reconstructPath(AStarNode endNode) {
List<Point> path = new ArrayList<>();
AStarNode current = endNode;
while (current != null) {
path.add(current.point);
current = current.parent;
}
Collections.reverse(path);
return path;
}
/**
* 在迷宫上可视化路径
*/
public static void printPath(MagicMaze maze, List<Point> path) {
CellType[][] display = maze.getGridCopy();
// 标记路径
for (Point p : path) {
if (display[p.y][p.x] != CellType.START && display[p.y][p.x] != CellType.END) {
display[p.y][p.x] = CellType.SOLUTION;
}
}
// 打印
for (int y = 0; y < maze.getHeight(); y++) {
for (int x = 0; x < maze.getWidth(); x++) {
System.out.print(display[y][x].getSymbol());
}
System.out.println();
}
}
/**
* 比较A*与BFS的性能
*/
public static void comparePerformance(MagicMaze maze, Point start, Point end) {
long startTime, endTime;
// A*搜索
startTime = System.nanoTime();
List<Point> astarPath = solve(maze, start, end);
endTime = System.nanoTime();
long astarTime = endTime - startTime;
// BFS搜索
startTime = System.nanoTime();
List<Point> bfsPath = bfsSolve(maze, start, end);
endTime = System.nanoTime();
long bfsTime = endTime - startTime;
System.out.println("===== 性能对比 =====");
System.out.println("A* 路径长度: " + (astarPath != null ? astarPath.size() : "无解"));
System.out.println("A* 耗时: " + astarTime / 1000 + " μs");
System.out.println("BFS 路径长度: " + (bfsPath != null ? bfsPath.size() : "无解"));
System.out.println("BFS 耗时: " + bfsTime / 1000 + " μs");
System.out.println("速度比 (BFS/A*): " + String.format("%.2f", (double)bfsTime / astarTime));
}
/**
* BFS求解(用于对比)
*/
public static List<Point> bfsSolve(MagicMaze maze, Point start, Point end) {
Queue<Point> queue = new LinkedList<>();
Map<Point, Point> parent = new HashMap<>();
Set<Point> visited = new HashSet<>();
queue.add(start);
visited.add(start);
int[][] directions = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
while (!queue.isEmpty()) {
Point current = queue.poll();
if (current.equals(end)) {
return reconstructBfsPath(parent, start, end);
}
for (int[] dir : directions) {
int nx = current.x + dir[0];
int ny = current.y + dir[1];
Point neighbor = new Point(nx, ny);
if (maze.isPassable(nx, ny) && !visited.contains(neighbor)) {
visited.add(neighbor);
parent.put(neighbor, current);
queue.add(neighbor);
}
}
}
return null;
}
private static List<Point> reconstructBfsPath(Map<Point, Point> parent, Point start, Point end) {
List<Point> path = new ArrayList<>();
Point current = end;
while (!current.equals(start)) {
path.add(current);
current = parent.get(current);
}
path.add(start);
Collections.reverse(path);
return path;
}
}
四、完整运行示例
/**
* 魔法迷宫主程序
*/
public class MagicMazeApp {
public static void main(String[] args) {
// 创建21x21的迷宫
System.out.println("=== 生成魔法迷宫 ===\n");
MagicMaze maze = new MagicMaze(21, 21);
System.out.println("迷宫结构:");
maze.print();
Point start = maze.getStart();
Point end = maze.getEnd();
System.out.println("\n起点: " + start + ", 终点: " + end);
// 使用A*求解
System.out.println("\n=== A*寻路求解 ===");
List<Point> path = AStarSolver.solve(maze, start, end);
if (path != null) {
System.out.println("找到路径!步数: " + path.size());
System.out.println("\n路径可视化(*表示路径):");
AStarSolver.printPath(maze, path);
// 打印详细路径
System.out.println("\n详细路径坐标:");
for (int i = 0; i < path.size(); i++) {
System.out.print(path.get(i));
if (i < path.size() - 1) System.out.print(" -> ");
if ((i + 1) % 5 == 0) System.out.println();
}
System.out.println();
} else {
System.out.println("未找到路径!");
}
// 性能对比
System.out.println("\n=== 性能对比测试 ===");
AStarSolver.comparePerformance(maze, start, end);
// 大规模测试
System.out.println("\n=== 大规模迷宫测试(51x51)===");
MagicMaze largeMaze = new MagicMaze(51, 51);
Point largeStart = largeMaze.getStart();
Point largeEnd = largeMaze.getEnd();
long t1 = System.nanoTime();
List<Point> largePath = AStarSolver.solve(largeMaze, largeStart, largeEnd);
long t2 = System.nanoTime();
System.out.println("迷宫大小: 51x51");
System.out.println("A*寻路耗时: " + (t2 - t1) / 1000 + " μs");
System.out.println("路径长度: " + (largePath != null ? largePath.size() : "无解"));
}
}
五、算法分析
| 指标 | 递归分割生成 | A*寻路 | BFS寻路 |
|---|---|---|---|
| 时间复杂度 | O(n log n) | O(b^d) | O(V+E) |
| 空间复杂度 | O(n) | O(b^d) | O(V) |
| 路径最优性 | N/A | 最优(可采纳启发式) | 最优 |
| 实际性能 | 毫秒级(100×100) | 微秒级(21×21) | 毫秒级(21×21) |
其中n为格子数,b为分支因子,d为解深度,V和E分别为图的顶点数和边数。
六、总结与扩展
本文实现了递归分割迷宫生成法和A*寻路算法。递归分割法生成的迷宫具有独特的”房间”结构,与DFS回溯法生成的长走廊风格形成鲜明对比。
扩展方向:
1. 多种生成算法对比:实现Prim算法、Kruskal算法、DFS回溯法生成迷宫并比较风格差异
2. 3D迷宫:将算法扩展到三维空间
3. 动态迷宫:实现会随时间变化的迷宫
4. 多目标寻路:在迷宫中寻找经过多个检查点的最优路径