游戏背景与问题引入
迷宫寻宝是经典的探险游戏:玩家从起点出发,在复杂的迷宫中寻找散落的宝藏,最终到达出口。与单纯的迷宫求解不同,寻宝问题需要同时解决路径搜索和目标选择两个子问题——先找到通往每个宝藏的最短路径,再决定以什么顺序收集所有宝藏才能使总路程最短。这本质上是旅行商问题(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实现了迷宫寻宝的完整系统,核心算法亮点包括:
- 递归回溯法生成完美迷宫,保证有解且路径唯一
- A*启发式搜索在大型迷宫中高效寻找最短路径
- 状态压缩动态规划精确求解多目标收集的最优顺序
- 最近邻贪心策略作为高效的近似解法
读者可以进一步扩展:加入宝藏价值权重(带权TSP)、时间限制约束、动态障碍物、以及多玩家竞争模式。