每日算法 — 使用java实现密室逃脱:图论建模与状态空间BFS最短路径求解

引言

密室逃脱是一种热门的实景解谜游戏:玩家被困在密室中,需要寻找线索、解开谜题、找到钥匙,最终通过一系列房间到达出口。本文将用Java实现一个密室逃脱自动求解器,核心采用图论建模将房间和通道转化为图结构,并通过状态空间BFS搜索最短逃脱路径。

问题建模

密室逃脱可以建模为状态空间搜索问题:

  • 状态:当前所在房间 + 已收集的物品/钥匙集合
  • 动作:移动到相邻房间、拾取物品、使用钥匙开门、触发机关
  • 目标:到达出口房间
  • 约束:某些门需要特定钥匙,某些机关需要特定物品触发

这种建模方式将密室逃脱转化为带约束的最短路径问题。

import java.util.*;

/**
 * 密室逃脱求解器:图论建模 + 状态空间BFS
 */
public class EscapeRoomSolver {

    // 物品类型
    enum Item { RED_KEY, BLUE_KEY, GOLD_KEY, LEVER, NOTE, TORCH }

    // 房间
    static class Room {
        int id;
        String name;
        Set<Item> items;           // 房间内可拾取的物品
        boolean isExit;            // 是否为出口

        Room(int id, String name) {
            this.id = id;
            this.name = name;
            this.items = new HashSet<>();
            this.isExit = false;
        }
    }

    // 通道(门)
    static class Door {
        int from, to;              // 连接的房间
        Item requiredKey;          // 所需钥匙(null表示无需钥匙)
        boolean isOneWay;          // 是否单向
        String puzzle;             // 谜题描述(简化处理)

        Door(int from, int to, Item key, boolean oneWay) {
            this.from = from;
            this.to = to;
            this.requiredKey = key;
            this.isOneWay = oneWay;
        }
    }

    // 游戏状态:位置 + 背包
    static class GameState {
        int roomId;                // 当前房间
        Set<Item> inventory;       // 已收集物品
        List<String> path;         // 到达此状态的路径记录
        int steps;                 // 已走步数

        GameState(int roomId, Set<Item> inventory, List<String> path, int steps) {
            this.roomId = roomId;
            this.inventory = new HashSet<>(inventory);
            this.path = new ArrayList<>(path);
            this.steps = steps;
        }

        // 状态唯一标识(用于判重)
        String getKey() {
            List<Item> sorted = new ArrayList<>(inventory);
            Collections.sort(sorted, Comparator.comparing(Enum::name));
            return roomId + "|" + sorted;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof GameState)) return false;
            GameState other = (GameState) o;
            return roomId == other.roomId && inventory.equals(other.inventory);
        }

        @Override
        public int hashCode() {
            return Objects.hash(roomId, inventory);
        }
    }

    private Map<Integer, Room> rooms;
    private Map<Integer, List<Door>> adjacency;  // 邻接表

    public EscapeRoomSolver() {
        rooms = new HashMap<>();
        adjacency = new HashMap<>();
    }

    public void addRoom(Room room) {
        rooms.put(room.id, room);
        adjacency.putIfAbsent(room.id, new ArrayList<>());
    }

    public void addDoor(Door door) {
        adjacency.get(door.from).add(door);
        if (!door.isOneWay) {
            // 添加反向通道(如果不需要钥匙,或者钥匙是对称的)
            // 简化处理:双向门共用同一把钥匙
            adjacency.get(door.to).add(new Door(door.to, door.from, door.requiredKey, false));
        }
    }

状态空间BFS搜索

    /**
     * BFS搜索最短逃脱路径
     * 核心:将(房间, 背包)作为复合状态,确保找到步数最少的解
     * @param startRoom 起始房间ID
     * @return 逃脱路径,若无法逃脱返回null
     */
    public List<String> solve(int startRoom) {
        Queue<GameState> queue = new LinkedList<>();
        Set<String> visited = new HashSet<>();

        // 初始状态:在起始房间,空背包
        GameState initial = new GameState(startRoom, new HashSet<>(), 
                                           new ArrayList<>(), 0);
        queue.offer(initial);
        visited.add(initial.getKey());

        while (!queue.isEmpty()) {
            GameState current = queue.poll();
            Room room = rooms.get(current.roomId);

            // 检查是否到达出口
            if (room.isExit) {
                current.path.add("到达出口! 总步数: " + current.steps);
                return current.path;
            }

            // 动作1:拾取当前房间的所有物品
            Set<Item> newInventory = new HashSet<>(current.inventory);
            boolean picked = false;
            for (Item item : new HashSet<>(room.items)) {
                if (!current.inventory.contains(item)) {
                    newInventory.add(item);
                    picked = true;
                }
            }

            if (picked) {
                GameState pickState = new GameState(current.roomId, newInventory,
                                                     current.path, current.steps + 1);
                pickState.path.add("在" + room.name + "拾取物品: " + 
                    diffSet(newInventory, current.inventory));

                String key = pickState.getKey();
                if (!visited.contains(key)) {
                    visited.add(key);
                    queue.offer(pickState);
                }
            }

            // 动作2:尝试通过所有可达的门
            for (Door door : adjacency.getOrDefault(current.roomId, new ArrayList<>())) {
                // 检查是否有钥匙
                if (door.requiredKey != null && 
                    !current.inventory.contains(door.requiredKey)) {
                    continue; // 缺少钥匙,无法通过
                }

                GameState nextState = new GameState(door.to, current.inventory,
                                                     current.path, current.steps + 1);
                Room nextRoom = rooms.get(door.to);
                String action = "从" + room.name + "移动到" + nextRoom.name;
                if (door.requiredKey != null) {
                    action += " (使用" + door.requiredKey + ")";
                }
                nextState.path.add(action);

                String key = nextState.getKey();
                if (!visited.contains(key)) {
                    visited.add(key);
                    queue.offer(nextState);
                }
            }
        }

        return null; // 无法逃脱
    }

    private Set<Item> diffSet(Set<Item> a, Set<Item> b) {
        Set<Item> diff = new HashSet<>(a);
        diff.removeAll(b);
        return diff;
    }

A*启发式搜索优化

对于复杂密室,纯BFS可能状态空间爆炸。引入A算法,使用曼哈顿距离+未获取关键物品数*作为启发函数。

    /**
     * A*搜索:带启发式的最优路径搜索
     */
    public List<String> solveAStar(int startRoom) {
        // 优先级队列:按 f = g + h 排序
        PriorityQueue<AStarNode> pq = new PriorityQueue<>(
            Comparator.comparingDouble(n -> n.f));
        Set<String> visited = new HashSet<>();

        GameState initial = new GameState(startRoom, new HashSet<>(), 
                                           new ArrayList<>(), 0);
        pq.offer(new AStarNode(initial, heuristic(initial)));
        visited.add(initial.getKey());

        while (!pq.isEmpty()) {
            AStarNode node = pq.poll();
            GameState current = node.state;
            Room room = rooms.get(current.roomId);

            if (room.isExit) {
                current.path.add("到达出口! 总步数: " + current.steps);
                return current.path;
            }

            // 生成所有后继状态(同BFS)
            List<GameState> successors = generateSuccessors(current);

            for (GameState next : successors) {
                String key = next.getKey();
                if (visited.contains(key)) continue;

                visited.add(key);
                double h = heuristic(next);
                pq.offer(new AStarNode(next, next.steps + h));
            }
        }

        return null;
    }

    /**
     * 启发函数:估计从当前状态到目标的最小代价
     * h = 到最近出口的最短路径 + 缺失关键物品数 * 惩罚
     */
    private double heuristic(GameState state) {
        // 计算到各出口的最短路径(忽略钥匙限制,作为下界)
        double minDist = Double.MAX_VALUE;
        for (Room room : rooms.values()) {
            if (room.isExit) {
                double dist = shortestPathDistance(state.roomId, room.id);
                minDist = Math.min(minDist, dist);
            }
        }

        // 统计还缺少多少把钥匙(所有门所需的钥匙集合)
        Set<Item> neededKeys = new HashSet<>();
        for (List<Door> doors : adjacency.values()) {
            for (Door door : doors) {
                if (door.requiredKey != null) {
                    neededKeys.add(door.requiredKey);
                }
            }
        }
        neededKeys.removeAll(state.inventory);

        return minDist + neededKeys.size() * 2; // 每缺少一把钥匙预估需要2步
    }

    /**
     * 忽略钥匙限制的简单最短路径(Dijkstra/BFS)
     */
    private double shortestPathDistance(int from, int to) {
        Queue<int[]> queue = new LinkedList<>();
        Set<Integer> visited = new HashSet<>();
        queue.offer(new int[]{from, 0});
        visited.add(from);

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            if (cur[0] == to) return cur[1];

            for (Door door : adjacency.getOrDefault(cur[0], new ArrayList<>())) {
                if (!visited.contains(door.to)) {
                    visited.add(door.to);
                    queue.offer(new int[]{door.to, cur[1] + 1});
                }
            }
        }
        return Double.MAX_VALUE;
    }

    static class AStarNode {
        GameState state;
        double f;  // f = g + h

        AStarNode(GameState state, double f) {
            this.state = state;
            this.f = f;
        }
    }

    private List<GameState> generateSuccessors(GameState current) {
        List<GameState> result = new ArrayList<>();
        Room room = rooms.get(current.roomId);

        // 拾取物品
        Set<Item> newInv = new HashSet<>(current.inventory);
        boolean picked = false;
        for (Item item : room.items) {
            if (!current.inventory.contains(item)) {
                newInv.add(item);
                picked = true;
            }
        }
        if (picked) {
            GameState s = new GameState(current.roomId, newInv, current.path, current.steps + 1);
            s.path.add("拾取: " + diffSet(newInv, current.inventory));
            result.add(s);
        }

        // 移动
        for (Door door : adjacency.getOrDefault(current.roomId, new ArrayList<>())) {
            if (door.requiredKey != null && !current.inventory.contains(door.requiredKey)) 
                continue;
            GameState s = new GameState(door.to, current.inventory, current.path, current.steps + 1);
            s.path.add("移动至" + rooms.get(door.to).name);
            result.add(s);
        }

        return result;
    }

完整测试:三层密室场景

    /**
     * 构建测试密室场景
     * 场景:三层密室,需要收集钥匙逐层解锁
     */
    public static EscapeRoomSolver buildTestScenario() {
        EscapeRoomSolver solver = new EscapeRoomSolver();

        // 创建房间
        Room entrance = new Room(0, "入口大厅");
        Room library = new Room(1, "图书馆");
        Room kitchen = new Room(2, "厨房");
        Room garden = new Room(3, "花园");
        Room basement = new Room(4, "地下室");
        Room lab = new Room(5, "实验室");
        Room exit = new Room(6, "出口");
        exit.isExit = true;

        solver.addRoom(entrance);
        solver.addRoom(library);
        solver.addRoom(kitchen);
        solver.addRoom(garden);
        solver.addRoom(basement);
        solver.addRoom(lab);
        solver.addRoom(exit);

        // 放置物品
        library.items.add(Item.RED_KEY);
        kitchen.items.add(Item.BLUE_KEY);
        garden.items.add(Item.TORCH);
        basement.items.add(Item.GOLD_KEY);
        lab.items.add(Item.NOTE);

        // 构建通道
        solver.addDoor(new Door(0, 1, null, false));   // 入口 <-> 图书馆
        solver.addDoor(new Door(0, 2, null, false));   // 入口 <-> 厨房
        solver.addDoor(new Door(1, 3, Item.RED_KEY, false)); // 图书馆 <-> 花园(需红钥匙)
        solver.addDoor(new Door(2, 4, Item.BLUE_KEY, false)); // 厨房 <-> 地下室(需蓝钥匙)
        solver.addDoor(new Door(3, 5, Item.TORCH, false));    // 花园 <-> 实验室(需火把)
        solver.addDoor(new Door(4, 5, Item.GOLD_KEY, false)); // 地下室 <-> 实验室(需金钥匙)
        solver.addDoor(new Door(5, 6, null, false));   // 实验室 <-> 出口

        return solver;
    }

    public static void main(String[] args) {
        EscapeRoomSolver solver = buildTestScenario();

        System.out.println("=== 密室逃脱求解 ===\n");
        System.out.println("场景描述:");
        System.out.println("入口大厅 -> 图书馆(红钥匙) -> 花园(火把) -> 实验室 -> 出口");
        System.out.println("入口大厅 -> 厨房(蓝钥匙) -> 地下室(金钥匙) -> 实验室 -> 出口\n");

        // BFS求解
        System.out.println("--- BFS最短路径 ---");
        List<String> bfsPath = solver.solve(0);
        if (bfsPath != null) {
            for (String step : bfsPath) {
                System.out.println(step);
            }
        } else {
            System.out.println("无法逃脱!");
        }

        System.out.println("\n--- A*启发式搜索 ---");
        List<String> astarPath = solver.solveAStar(0);
        if (astarPath != null) {
            for (String step : astarPath) {
                System.out.println(step);
            }
        }
    }
}

复杂度分析

  • 状态空间大小:设 $R$ 为房间数,$I$ 为物品总数,则最大状态数为 $R \cdot 2^I$
  • BFS时间复杂度:$O(R \cdot 2^I)$,每个状态处理一次
  • BFS空间复杂度:$O(R \cdot 2^I)$,存储访问标记和队列
  • A*优化:在启发函数良好的情况下,实际访问的状态数远小于BFS,通常可缩减至 $O(R \cdot 2^{I-k})$,$k$ 为已收集物品数

扩展:带机关触发机制的求解

    // 机关类型
    static class Mechanism {
        int roomId;              // 所在房间
        Item triggerItem;        // 触发所需物品
        int unlocksRoom;         // 解锁的房间
        String description;

        Mechanism(int room, Item item, int unlocks, String desc) {
            this.roomId = room;
            this.triggerItem = item;
            this.unlocksRoom = unlocks;
            this.description = desc;
        }
    }

    private List<Mechanism> mechanisms = new ArrayList<>();

    /**
     * 扩展的状态转移:增加机关触发动作
     */
    private List<GameState> generateSuccessorsWithMechanisms(GameState current) {
        List<GameState> result = generateSuccessors(current);

        // 检查当前房间是否有可触发的机关
        for (Mechanism mech : mechanisms) {
            if (mech.roomId == current.roomId && 
                current.inventory.contains(mech.triggerItem)) {
                // 机关触发后解锁新通道
                // 实际实现中需要动态修改邻接表或记录解锁状态
                GameState triggered = new GameState(current.roomId, current.inventory,
                                                     current.path, current.steps + 1);
                triggered.path.add("触发机关: " + mech.description);
                result.add(triggered);
            }
        }

        return result;
    }

总结

本文通过Java实现了密室逃脱问题的完整求解框架:

  1. 图论建模:将房间映射为节点,通道映射为带权边(钥匙约束)
  2. 状态空间复合:将(位置, 背包)作为搜索状态,保证完备性
  3. BFS最短路径:确保找到的逃脱方案步数最少
  4. A*启发优化:利用距离启发函数加速复杂密室求解

读者可以进一步扩展:引入时间压力机制、增加NPC交互、实现多玩家协同逃脱、或构建随机密室生成器。