每日算法 — 使用java实现密室逃脱:约束满足问题与回溯搜索求解

引言

密室逃脱要求玩家在有限空间内寻找线索、解开谜题、最终逃脱。从算法角度看,每个谜题都是一个约束满足问题(CSP),而整个逃脱过程则是状态空间中的搜索问题。本文用Java实现一个密室逃脱求解框架,展示如何将复杂谜题建模为CSP并用回溯搜索求解。

核心算法设计

1. 谜题建模为约束满足问题

将密室中的每个谜题抽象为变量、域和约束的三元组。

import java.util.*;

/**
 * 约束满足问题中的变量
 * 代表谜题中的一个待解元素,如密码锁的一位数字
 */
class CSPVariable {
    String name;
    List<Integer> domain; // 取值域
    Integer assignedValue; // 已分配的值(null表示未分配)

    public CSPVariable(String name, List<Integer> domain) {
        this.name = name;
        this.domain = new ArrayList<>(domain);
        this.assignedValue = null;
    }

    /**
     * 前向检查:当变量被赋值后,从其邻居的域中移除冲突值
     */
    public boolean assignAndForwardCheck(int value, List<CSPVariable> neighbors, List<Constraint> constraints) {
        this.assignedValue = value;
        List<Integer>[] removed = new List[neighbors.size()];
        for (int i = 0; i < neighbors.size(); i++) {
            removed[i] = new ArrayList<>();
            CSPVariable neighbor = neighbors.get(i);
            if (neighbor.assignedValue != null) continue;

            Iterator<Integer> it = neighbor.domain.iterator();
            while (it.hasNext()) {
                int val = it.next();
                // 检查此赋值是否与当前约束冲突
                for (Constraint c : constraints) {
                    if (c.involves(this, neighbor)) {
                        if (!c.satisfied(this, value, neighbor, val)) {
                            removed[i].add(val);
                            it.remove();
                            break;
                        }
                    }
                }
            }
            if (neighbor.domain.isEmpty()) {
                // 域为空,回溯
                undoForwardCheck(neighbors, removed);
                this.assignedValue = null;
                return false;
            }
        }
        return true;
    }

    private void undoForwardCheck(List<CSPVariable> neighbors, List<Integer>[] removed) {
        for (int i = 0; i < neighbors.size(); i++) {
            neighbors.get(i).domain.addAll(removed[i]);
        }
    }
}

/**
 * 约束接口
 */
interface Constraint {
    boolean involves(CSPVariable v1, CSPVariable v2);
    boolean satisfied(CSPVariable v1, int val1, CSPVariable v2, int val2);
}

/**
 * 不等约束:两个变量取值不同(如密码锁相邻位不能相同)
 */
class NotEqualConstraint implements Constraint {
    public boolean involves(CSPVariable v1, CSPVariable v2) {
        return true; // 简化:假设所有变量间都有不等约束
    }
    public boolean satisfied(CSPVariable v1, int val1, CSPVariable v2, int val2) {
        return val1 != val2;
    }
}

/**
 * 和约束:若干变量之和等于目标值
 */
class SumConstraint implements Constraint {
    List<CSPVariable> variables;
    int targetSum;

    public SumConstraint(List<CSPVariable> variables, int targetSum) {
        this.variables = variables;
        this.targetSum = targetSum;
    }

    public boolean involves(CSPVariable v1, CSPVariable v2) {
        return variables.contains(v1) && variables.contains(v2);
    }

    public boolean satisfied(CSPVariable v1, int val1, CSPVariable v2, int val2) {
        // 仅当所有变量都赋值时才完全检验
        int sum = val1 + val2;
        int unassigned = 0;
        for (CSPVariable v : variables) {
            if (v == v1 || v == v2) continue;
            if (v.assignedValue != null) sum += v.assignedValue;
            else unassigned++;
        }
        // 若还有未赋值变量,检查当前和是否可能达到目标
        if (unassigned > 0) {
            int minPossible = sum + unassigned * 0; // 最小值
            int maxPossible = sum + unassigned * 9; // 最大值(假设数字0-9)
            return targetSum >= minPossible && targetSum <= maxPossible;
        }
        return sum == targetSum;
    }
}

/**
 * 密室逃脱谜题求解器
 * 使用回溯搜索 + 前向检查 + MRV启发式
 */
class EscapeRoomSolver {
    List<CSPVariable> variables;
    List<Constraint> constraints;
    int nodeCount = 0; // 搜索节点计数

    public EscapeRoomSolver(List<CSPVariable> variables, List<Constraint> constraints) {
        this.variables = variables;
        this.constraints = constraints;
    }

    /**
     * 主求解入口
     * @return 是否找到解
     */
    public boolean solve() {
        return backtrack();
    }

    /**
     * 回溯搜索核心
     * 1. 选择未赋值变量(MRV启发式:选域最小的)
     * 2. 依次尝试域中的值
     * 3. 前向检查约束一致性
     * 4. 递归或回溯
     */
    private boolean backtrack() {
        CSPVariable var = selectUnassignedVariableMRV();
        if (var == null) return true; // 所有变量已赋值,找到解

        List<Integer> domainCopy = new ArrayList<>(var.domain);
        for (int value : domainCopy) {
            nodeCount++;
            List<CSPVariable> neighbors = getNeighbors(var);
            boolean consistent = var.assignAndForwardCheck(value, neighbors, constraints);
            if (consistent) {
                if (backtrack()) return true;
            }
            // 回溯:撤销赋值
            var.assignedValue = null;
            // 前向检查已自动撤销域修改
        }
        return false;
    }

    /**
     * MRV启发式:选择剩余有效赋值最少的变量
     * 这能显著减少搜索分支
     */
    private CSPVariable selectUnassignedVariableMRV() {
        CSPVariable best = null;
        int minDomainSize = Integer.MAX_VALUE;
        for (CSPVariable v : variables) {
            if (v.assignedValue == null && v.domain.size() < minDomainSize) {
                minDomainSize = v.domain.size();
                best = v;
            }
        }
        return best;
    }

    private List<CSPVariable> getNeighbors(CSPVariable var) {
        List<CSPVariable> neighbors = new ArrayList<>();
        for (CSPVariable v : variables) {
            if (v != var && v.assignedValue == null) {
                for (Constraint c : constraints) {
                    if (c.involves(var, v)) {
                        neighbors.add(v);
                        break;
                    }
                }
            }
        }
        return neighbors;
    }

    public void printSolution() {
        System.out.println("=== 密室逃脱谜题解答 ===");
        for (CSPVariable v : variables) {
            System.out.printf("%s = %d\n", v.name, v.assignedValue);
        }
        System.out.println("搜索节点数: " + nodeCount);
    }
}

2. 密室状态空间搜索

除了单个谜题,整个密室逃脱还涉及多个房间的状态转移。我们实现一个状态空间搜索来规划逃脱路径。

/**
 * 密室房间状态
 */
class RoomState {
    String roomName;
    Set<String> collectedItems; // 已收集的道具
    Set<String> solvedPuzzles;  // 已解开的谜题
    Set<String> unlockedDoors;  // 已解锁的门

    public RoomState(String roomName) {
        this.roomName = roomName;
        this.collectedItems = new HashSet<>();
        this.solvedPuzzles = new HashSet<>();
        this.unlockedDoors = new HashSet<>();
    }

    public RoomState copy() {
        RoomState s = new RoomState(this.roomName);
        s.collectedItems.addAll(this.collectedItems);
        s.solvedPuzzles.addAll(this.solvedPuzzles);
        s.unlockedDoors.addAll(this.unlockedDoors);
        return s;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof RoomState)) return false;
        RoomState other = (RoomState) o;
        return roomName.equals(other.roomName)
            && collectedItems.equals(other.collectedItems)
            && solvedPuzzles.equals(other.solvedPuzzles);
    }

    @Override
    public int hashCode() {
        return Objects.hash(roomName, collectedItems, solvedPuzzles);
    }
}

/**
 * 动作定义:玩家在密室中可执行的操作
 */
interface Action {
    boolean isApplicable(RoomState state);
    RoomState apply(RoomState state);
    String getName();
}

/**
 * 解谜动作
 */
class SolvePuzzleAction implements Action {
    String puzzleId;
    String requiredItem;
    String rewardItem;

    public SolvePuzzleAction(String puzzleId, String requiredItem, String rewardItem) {
        this.puzzleId = puzzleId;
        this.requiredItem = requiredItem;
        this.rewardItem = rewardItem;
    }

    public boolean isApplicable(RoomState state) {
        return !state.solvedPuzzles.contains(puzzleId)
            && (requiredItem == null || state.collectedItems.contains(requiredItem));
    }

    public RoomState apply(RoomState state) {
        RoomState next = state.copy();
        next.solvedPuzzles.add(puzzleId);
        if (rewardItem != null) next.collectedItems.add(rewardItem);
        return next;
    }

    public String getName() { return "解谜:" + puzzleId; }
}

/**
 * 密室逃脱路径规划器
 * 使用BFS在状态空间中搜索最短逃脱路径
 */
class EscapePathPlanner {

    /**
     * BFS搜索最短逃脱路径
     * @param initial 初始状态
     * @param goalChecker 目标状态判定
     * @param actions 可用动作列表
     * @return 动作序列
     */
    public List<Action> findShortestPath(RoomState initial,
                                          java.util.function.Predicate<RoomState> goalChecker,
                                          List<Action> actions) {
        Queue<RoomState> queue = new LinkedList<>();
        Map<RoomState, RoomState> parent = new HashMap<>();
        Map<RoomState, Action> actionTo = new HashMap<>();
        Set<RoomState> visited = new HashSet<>();

        queue.add(initial);
        visited.add(initial);

        while (!queue.isEmpty()) {
            RoomState current = queue.poll();
            if (goalChecker.test(current)) {
                return reconstructPath(initial, current, parent, actionTo);
            }

            for (Action action : actions) {
                if (action.isApplicable(current)) {
                    RoomState next = action.apply(current);
                    if (!visited.contains(next)) {
                        visited.add(next);
                        parent.put(next, current);
                        actionTo.put(next, action);
                        queue.add(next);
                    }
                }
            }
        }
        return null; // 无解
    }

    private List<Action> reconstructPath(RoomState start, RoomState goal,
                                          Map<RoomState, RoomState> parent,
                                          Map<RoomState, Action> actionTo) {
        LinkedList<Action> path = new LinkedList<>();
        RoomState current = goal;
        while (!current.equals(start)) {
            path.addFirst(actionTo.get(current));
            current = parent.get(current);
        }
        return path;
    }
}

完整测试案例

public class EscapeRoomDemo {
    public static void main(String[] args) {
        // 构造一个4位密码锁谜题
        // 约束:4位数字之和为15,相邻位不等,第一位是奇数
        List<CSPVariable> vars = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            vars.add(new CSPVariable("digit_" + i, Arrays.asList(0,1,2,3,4,5,6,7,8,9)));
        }

        // 添加第一位为奇数的约束(通过修改域实现)
        vars.get(0).domain = Arrays.asList(1,3,5,7,9);

        List<Constraint> constraints = new ArrayList<>();
        constraints.add(new NotEqualConstraint());
        constraints.add(new SumConstraint(vars, 15));

        EscapeRoomSolver solver = new EscapeRoomSolver(vars, constraints);
        boolean solved = solver.solve();
        if (solved) {
            solver.printSolution();
        } else {
            System.out.println("此谜题无解!");
        }
    }
}

复杂度分析

  • 回溯搜索最坏情况:O(dⁿ),d为域大小,n为变量数
  • MRV启发式 + 前向检查:实际中可将搜索空间减少数个数量级
  • BFS状态空间:O(bˢ),b为分支因子,s为最短解长度

总结

本文将密室逃脱抽象为约束满足问题与状态空间搜索的组合。CSP求解器通过回溯、前向检查与MRV启发式高效求解单个谜题;BFS路径规划器则在宏观层面寻找最优逃脱顺序。读者可扩展更复杂的约束类型(如线性方程、排列约束)来应对更困难的密室谜题。