每日算法 — 使用java实现迷宫寻宝:A*搜索与多目标路径规划

游戏背景与问题引入

迷宫寻宝是经典的探险游戏:玩家从起点出发,在复杂的迷宫中寻找散落的宝藏,最终到达出口。与单纯的迷宫求解不同,寻宝问题需要同时解决路径搜索和目标选择两个子问题——先找到通往每个宝藏的最短路径,再决定以什么顺序收集所有宝藏才能使总路程最短。这本质上是旅行商问题(TSP)在迷宫环境下的变种。本文将用Java实现一套完整的迷宫寻宝系统,结合A*搜索与多目标规划算法。

核心算法原理

1. A*启发式搜索

A*算法是求解最短路径的经典算法,其评估函数为:

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

  • g(n):从起点到节点n的实际代价(已走路径长度)
  • h(n):从节点n到目标的估计代价(启发函数)

对于网格迷宫,常用的启发函数:
– 曼哈顿距离:|x₁-x₂| + |y₁-y₂|(四方向移动)
– 欧几里得距离:√((x₁-x₂)² + (y₁-y₂)²)(八方向移动)

A保证最优性的条件:h(n) ≤ h(n)(启发函数不可高估实际代价)。

2. 多目标路径规划

当迷宫中有多个宝藏时,需要决定收集顺序。设宝藏位置为T₁, T₂, …, Tₙ,问题转化为:

寻找访问所有宝藏的最短哈密顿路径

对于n个宝藏,共有n!种排列。精确求解是NP-hard问题。本实现采用以下策略:

策略一:最近邻贪心
从当前位置出发,每次选择距离最近的未访问宝藏。

策略二:动态规划(状态压缩)
当宝藏数n ≤ 15时,可用状态压缩DP在O(n²·2ⁿ)时间内求得最优解。

策略三:两阶段优化
先用贪心获得初始解,再用2-opt局部搜索进行优化。

3. 迷宫生成(附赠算法)

本实现使用递归回溯法生成完美迷宫(无环、全连通):
1. 从起点开始,随机选择一个未访问的相邻单元格
2. 打通两单元格之间的墙
3. 递归对新单元格执行步骤1-2
4. 当无未访问邻居时,回溯到上一个单元格

Java实现

import java.util.*;

/**
 * 迷宫寻宝系统
 * 核心算法:A*搜索 + 多目标TSP规划 + 迷宫生成
 */
public class MazeTreasureHunt {

    // 迷宫单元格类型
    public static final int WALL = 0;
    public static final int PATH = 1;
    public static final int START = 2;
    public static final int EXIT = 3;
    public static final int TREASURE = 4;

    private final int rows, cols;
    private final int[][] maze;
    private final Random rand = new Random(42);

    // 起点、出口、宝藏位置
    private int startR, startC;
    private int exitR, exitC;
    private final List<int[]> treasures = new ArrayList<>();

    public MazeTreasureHunt(int rows, int cols) {
        this.rows = rows * 2 + 1;
        this.cols = cols * 2 + 1;
        this.maze = new int[this.rows][this.cols];
        generateMaze(rows, cols);
    }

    /**
     * 递归回溯法生成完美迷宫
     */
    private void generateMaze(int h, int w) {
        // 初始化为全墙
        for (int[] row : maze) Arrays.fill(row, WALL);

        // 使用DFS生成迷宫(在奇数坐标上操作)
        boolean[][] visited = new boolean[h][w];
        Stack<int[]> stack = new Stack<>();
        int sr = rand.nextInt(h);
        int sc = rand.nextInt(w);
        visited[sr][sc] = true;
        stack.push(new int[]{sr, sc});
        maze[sr * 2 + 1][sc * 2 + 1] = PATH;

        int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

        while (!stack.isEmpty()) {
            int[] cur = stack.peek();
            int cr = cur[0], cc = cur[1];

            // 收集未访问的邻居
            List<int[]> neighbors = new ArrayList<>();
            for (int[] d : dirs) {
                int nr = cr + d[0], nc = cc + d[1];
                if (nr >= 0 && nr < h && nc >= 0 && nc < w && !visited[nr][nc]) {
                    neighbors.add(new int[]{nr, nc, d[0], d[1]});
                }
            }

            if (neighbors.isEmpty()) {
                stack.pop();
            } else {
                int[] next = neighbors.get(rand.nextInt(neighbors.size()));
                int nr = next[0], nc = next[1];
                int dr = next[2], dc = next[3];
                visited[nr][nc] = true;
                // 打通墙壁
                maze[cr * 2 + 1 + dr][cc * 2 + 1 + dc] = PATH;
                maze[nr * 2 + 1][nc * 2 + 1] = PATH;
                stack.push(new int[]{nr, nc});
            }
        }

        // 设置起点(左上角通路)
        startR = 1; startC = 1;
        maze[startR][startC] = START;

        // 设置出口(右下角通路)
        exitR = this.rows - 2; exitC = this.cols - 2;
        maze[exitR][exitC] = EXIT;

        // 随机放置宝藏(在通路上)
        int treasureCount = Math.min(5, h * w / 10);
        while (treasures.size() < treasureCount) {
            int tr = rand.nextInt(this.rows);
            int tc = rand.nextInt(this.cols);
            if (maze[tr][tc] == PATH) {
                maze[tr][tc] = TREASURE;
                treasures.add(new int[]{tr, tc});
            }
        }
    }

    // ==================== A*搜索 ====================

    /**
     * A*搜索寻找最短路径
     * @param sr 起点行
     * @param sc 起点列
     * @param er 终点行
     * @param ec 终点列
     * @return 路径点列表,若无路径返回null
     */
    public List<int[]> aStar(int sr, int sc, int er, int ec) {
        // 优先队列:按f值排序
        PriorityQueue<Node> open = new PriorityQueue<>(Comparator.comparingDouble(n -> n.f));
        boolean[][] closed = new boolean[rows][cols];
        double[][] gScore = new double[rows][cols];
        for (double[] row : gScore) Arrays.fill(row, Double.MAX_VALUE);
        int[][][] parent = new int[rows][cols][2]; // 记录父节点

        gScore[sr][sc] = 0;
        open.add(new Node(sr, sc, 0, heuristic(sr, sc, er, ec)));

        int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

        while (!open.isEmpty()) {
            Node cur = open.poll();
            int r = cur.r, c = cur.c;

            if (closed[r][c]) continue;
            closed[r][c] = true;

            if (r == er && c == ec) {
                return reconstructPath(parent, sr, sc, er, ec);
            }

            for (int[] d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
                if (maze[nr][nc] == WALL || closed[nr][nc]) continue;

                double tentativeG = gScore[r][c] + 1;
                if (tentativeG < gScore[nr][nc]) {
                    gScore[nr][nc] = tentativeG;
                    parent[nr][nc] = new int[]{r, c};
                    double f = tentativeG + heuristic(nr, nc, er, ec);
                    open.add(new Node(nr, nc, tentativeG, f));
                }
            }
        }
        return null; // 无路径
    }

    /**
     * 曼哈顿距离启发函数
     */
    private double heuristic(int r1, int c1, int r2, int c2) {
        return Math.abs(r1 - r2) + Math.abs(c1 - c2);
    }

    private List<int[]> reconstructPath(int[][][] parent, int sr, int sc, int er, int ec) {
        List<int[]> path = new ArrayList<>();
        int r = er, c = ec;
        while (r != sr || c != sc) {
            path.add(new int[]{r, c});
            int[] p = parent[r][c];
            r = p[0]; c = p[1];
        }
        path.add(new int[]{sr, sc});
        Collections.reverse(path);
        return path;
    }

    static class Node {
        int r, c;
        double g, f;
        Node(int r, int c, double g, double f) { this.r = r; this.c = c; this.g = g; this.f = f; }
    }

    // ==================== 多目标规划 ====================

    /**
     * 最近邻贪心策略:规划寻宝顺序
     */
    public List<int[]> nearestNeighborOrder() {
        List<int[]> order = new ArrayList<>();
        List<int[]> remaining = new ArrayList<>(treasures);
        int curR = startR, curC = startC;

        while (!remaining.isEmpty()) {
            int[] nearest = null;
            double minDist = Double.MAX_VALUE;
            List<int[]> bestPath = null;

            for (int[] t : remaining) {
                List<int[]> path = aStar(curR, curC, t[0], t[1]);
                if (path != null && path.size() < minDist) {
                    minDist = path.size();
                    nearest = t;
                    bestPath = path;
                }
            }

            if (nearest == null) break;
            order.add(nearest);
            remaining.remove(nearest);
            curR = nearest[0];
            curC = nearest[1];
        }
        return order;
    }

    /**
     * 状态压缩DP求解最优顺序(适用于宝藏数<=15)
     * @return [最优顺序列表, 总路径长度]
     */
    public Object[] optimalOrderDP() {
        int n = treasures.size();
        if (n > 15) {
            System.out.println("宝藏过多,使用贪心策略代替DP");
            return new Object[]{nearestNeighborOrder(), -1};
        }

        // 预计算所有点对之间的最短距离
        int[][] dist = new int[n + 2][n + 2]; // 0=start, 1..n=treasures, n+1=exit
        int[][][] allPaths = new int[n + 2][n + 2][];

        // 起点到各宝藏
        for (int i = 0; i < n; i++) {
            List<int[]> path = aStar(startR, startC, treasures.get(i)[0], treasures.get(i)[1]);
            dist[0][i + 1] = path != null ? path.size() : Integer.MAX_VALUE;
        }
        // 宝藏之间
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                List<int[]> path = aStar(treasures.get(i)[0], treasures.get(i)[1],
                        treasures.get(j)[0], treasures.get(j)[1]);
                dist[i + 1][j + 1] = path != null ? path.size() : Integer.MAX_VALUE;
            }
        }
        // 宝藏到出口
        for (int i = 0; i < n; i++) {
            List<int[]> path = aStar(treasures.get(i)[0], treasures.get(i)[1], exitR, exitC);
            dist[i + 1][n + 1] = path != null ? path.size() : Integer.MAX_VALUE;
        }

        // DP[mask][i] = 已访问mask集合的宝藏,最后停在第i个宝藏的最短距离
        int size = 1 << n;
        int[][] dp = new int[size][n];
        int[][] prev = new int[size][n];
        for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE / 2);

        // 初始化:从起点到每个宝藏
        for (int i = 0; i < n; i++) {
            dp[1 << i][i] = dist[0][i + 1];
        }

        // 状态转移
        for (int mask = 1; mask < size; mask++) {
            for (int last = 0; last < n; last++) {
                if ((mask & (1 << last)) == 0) continue;
                if (dp[mask][last] >= Integer.MAX_VALUE / 2) continue;
                for (int next = 0; next < n; next++) {
                    if ((mask & (1 << next)) != 0) continue;
                    int newMask = mask | (1 << next);
                    int newDist = dp[mask][last] + dist[last + 1][next + 1];
                    if (newDist < dp[newMask][next]) {
                        dp[newMask][next] = newDist;
                        prev[newMask][next] = last;
                    }
                }
            }
        }

        // 找到最优终点
        int fullMask = size - 1;
        int bestLast = 0;
        int bestTotal = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int total = dp[fullMask][i] + dist[i + 1][n + 1];
            if (total < bestTotal) {
                bestTotal = total;
                bestLast = i;
            }
        }

        // 回溯路径
        List<int[]> order = new ArrayList<>();
        int mask = fullMask;
        int cur = bestLast;
        for (int i = 0; i < n; i++) {
            order.add(treasures.get(cur));
            int p = prev[mask][cur];
            mask ^= (1 << cur);
            cur = p;
        }
        Collections.reverse(order);
        return new Object[]{order, bestTotal};
    }

    /**
     * 打印迷宫
     */
    public void printMaze() {
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                switch (maze[r][c]) {
                    case WALL: System.out.print("██"); break;
                    case PATH: System.out.print("  "); break;
                    case START: System.out.print(" S"); break;
                    case EXIT: System.out.print(" E"); break;
                    case TREASURE: System.out.print(" T"); break;
                }
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        System.out.println("=== 迷宫寻宝系统 ===
");

        // 生成10x10的迷宫
        MazeTreasureHunt game = new MazeTreasureHunt(10, 10);
        System.out.println("迷宫布局(S=起点, E=出口, T=宝藏):");
        game.printMaze();

        System.out.println("
【阶段1】A*搜索测试:起点到出口的最短路径");
        List<int[]> pathToExit = game.aStar(game.startR, game.startC, game.exitR, game.exitC);
        if (pathToExit != null) {
            System.out.println("  最短路径长度: " + pathToExit.size() + " 步");
        }

        System.out.println("
【阶段2】最近邻贪心策略");
        List<int[]> greedyOrder = game.nearestNeighborOrder();
        System.out.println("  收集顺序(贪心):");
        int totalGreedy = 0;
        int curR = game.startR, curC = game.startC;
        for (int[] t : greedyOrder) {
            List<int[]> p = game.aStar(curR, curC, t[0], t[1]);
            totalGreedy += p.size();
            System.out.printf("    → 宝藏(%d,%d), 距离=%d步
", t[0], t[1], p.size());
            curR = t[0]; curC = t[1];
        }
        List<int[]> toExit = game.aStar(curR, curC, game.exitR, game.exitC);
        totalGreedy += toExit.size();
        System.out.println("  贪心总路程: " + totalGreedy + " 步");

        System.out.println("
【阶段3】状态压缩DP最优策略");
        Object[] result = game.optimalOrderDP();
        List<int[]> optimalOrder = (List<int[]>) result[0];
        int optimalTotal = (int) result[1];
        System.out.println("  收集顺序(最优):");
        for (int[] t : optimalOrder) {
            System.out.printf("    → 宝藏(%d,%d)
", t[0], t[1]);
        }
        System.out.println("  DP最优总路程: " + optimalTotal + " 步");

        if (optimalTotal > 0) {
            System.out.printf("
  贪心 vs 最优: %d vs %d (差距 %.1f%%)
",
                    totalGreedy, optimalTotal,
                    (totalGreedy - optimalTotal) * 100.0 / optimalTotal);
        }

        System.out.println("
=== 核心结论 ===");
        System.out.println("1. A*搜索结合曼哈顿距离启发函数高效求解迷宫最短路径");
        System.out.println("2. 多目标规划可用状态压缩DP在O(n²·2ⁿ)内求得精确最优解");
        System.out.println("3. 最近邻贪心策略简单高效,但在复杂场景下与最优解存在差距");
    }
}

复杂度分析

  • A*搜索时间复杂度:O(b^d),其中b为分支因子,d为解深度。实际中由于启发函数的引导,通常远优于BFS的O(V+E)。
  • A*空间复杂度:O(V),V为迷宫单元格数。
  • 状态压缩DP时间复杂度:O(n²·2ⁿ),n为宝藏数量。适用于n ≤ 15的场景。
  • 最近邻贪心时间复杂度:O(n²·A),其中A为单次搜索开销。

总结

本文通过Java实现了迷宫寻宝的完整系统,核心算法亮点包括:

  1. 递归回溯法生成完美迷宫,保证有解且路径唯一
  2. A*启发式搜索在大型迷宫中高效寻找最短路径
  3. 状态压缩动态规划精确求解多目标收集的最优顺序
  4. 最近邻贪心策略作为高效的近似解法

读者可以进一步扩展:加入宝藏价值权重(带权TSP)、时间限制约束、动态障碍物、以及多玩家竞争模式。