每日算法 — 使用java实现叠叠杯:状态空间BFS与最优操作序列规划

游戏简介与算法背景

叠叠杯(Cup Stacking)是一款考验手眼协调的经典益智玩具:若干颜色各异的杯子以特定规则堆叠,玩家需要通过一系列移动操作将杯子从初始状态整理为目标状态。从算法视角看,这等价于状态空间搜索问题——每个杯子的堆叠配置是一个状态,合法移动是状态转移边,求解最短操作序列即寻找状态空间中的最短路径。本文用 Java 实现基于 BFS 与 A* 搜索的最优解算法。

核心算法设计

1. 状态表示

将每个杯子堆叠列视为一个栈,整体状态用一个栈数组表示。状态编码为字符串以便哈希判重。

2. BFS 最短路径

BFS 天然保证找到的操作步数最少。核心流程:
– 将初始状态入队
– 逐层扩展所有合法移动
– 首次到达目标状态即为最优解

3. A* 启发式优化

对于复杂状态,BFS 的状态爆炸严重。引入启发函数 f = g + h:
– g:已执行步数
– h:错位杯子数 / 2(每个错误杯子至少需要一次移动)

Java 完整实现

import java.util.*;

/**
 * 叠叠杯游戏 — 状态空间BFS与A*最优操作序列规划
 * 
 * 核心算法:
 * 1. 状态空间BFS:保证最少步数的完备搜索
 * 2. A*搜索:启发式加速,适合复杂初始状态
 * 3. 状态哈希编码:字符串化判重
 * 4. 路径回溯:记录前驱状态与操作
 */
public class CupStacking {

    private final int cupCount;
    private final int columnCount;

    public CupStacking(int cupCount, int columnCount) {
        this.cupCount = cupCount;
        this.columnCount = columnCount;
    }

    /**
     * 状态类:不可变,便于哈希和比较
     */
    static class State {
        final List<Stack<Integer>> columns; // 每列是一个杯子栈,栈顶在末尾
        final String encoded;               // 编码字符串,用于哈希判重

        State(List<Stack<Integer>> cols) {
            this.columns = new ArrayList<>();
            for (Stack<Integer> s : cols) {
                Stack<Integer> copy = new Stack<>();
                copy.addAll(s);
                this.columns.add(copy);
            }
            this.encoded = encode();
        }

        /**
         * 将状态编码为字符串,格式:每列杯子用逗号分隔,列之间用分号分隔
         * 例如 "1,2;3;" 表示第一列有杯子1和2(1在底),第二列有杯子3
         */
        private String encode() {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < columns.size(); i++) {
                Stack<Integer> col = columns.get(i);
                for (int j = 0; j < col.size(); j++) {
                    if (j > 0) sb.append(',');
                    sb.append(col.get(j));
                }
                if (i < columns.size() - 1) sb.append(';');
            }
            return sb.toString();
        }

        /**
         * 获取可执行的移动:将某列顶部杯子移到另一非空列或空列
         */
        List<Move> getLegalMoves() {
            List<Move> moves = new ArrayList<>();
            for (int from = 0; from < columns.size(); from++) {
                if (columns.get(from).isEmpty()) continue;
                for (int to = 0; to < columns.size(); to++) {
                    if (from == to) continue;
                    moves.add(new Move(from, to));
                }
            }
            return moves;
        }

        /**
         * 执行移动,返回新状态
         */
        State applyMove(Move move) {
            List<Stack<Integer>> newCols = new ArrayList<>();
            for (Stack<Integer> s : columns) {
                Stack<Integer> copy = new Stack<>();
                copy.addAll(s);
                newCols.add(copy);
            }
            int cup = newCols.get(move.from).pop();
            newCols.get(move.to).push(cup);
            return new State(newCols);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof State)) return false;
            return encoded.equals(((State) o).encoded);
        }

        @Override
        public int hashCode() {
            return encoded.hashCode();
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < columns.size(); i++) {
                sb.append("列").append(i).append(": ");
                sb.append(columns.get(i));
                sb.append("\n");
            }
            return sb.toString();
        }
    }

    /**
     * 移动操作:从 from 列顶部移一个杯子到 to 列
     */
    static class Move {
        final int from;
        final int to;

        Move(int from, int to) {
            this.from = from;
            this.to = to;
        }

        @Override
        public String toString() {
            return String.format("列%d → 列%d", from, to);
        }
    }

    /**
     * BFS求解最短操作序列
     * 时间复杂度:O(状态数 × 分支因子)
     * 空间复杂度:O(状态数)
     */
    public List<Move> solveBFS(State initial, State target) {
        if (initial.equals(target)) {
            return new ArrayList<>();
        }

        Queue<State> queue = new LinkedList<>();
        Set<String> visited = new HashSet<>();
        Map<String, State> parent = new HashMap<>();    // 前驱状态
        Map<String, Move> parentMove = new HashMap<>(); // 导致该状态的操作

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

        while (!queue.isEmpty()) {
            State current = queue.poll();

            for (Move move : current.getLegalMoves()) {
                State next = current.applyMove(move);

                if (visited.contains(next.encoded)) {
                    continue;
                }

                visited.add(next.encoded);
                parent.put(next.encoded, current);
                parentMove.put(next.encoded, move);

                if (next.equals(target)) {
                    return reconstructPath(parent, parentMove, initial, next);
                }

                queue.offer(next);
            }
        }

        return null; // 无解
    }

    /**
     * A*搜索:启发式加速
     * 启发函数:不在目标位置的杯子数 / 2
     */
    public List<Move> solveAStar(State initial, State target) {
        if (initial.equals(target)) {
            return new ArrayList<>();
        }

        // 优先队列:按 f = g + h 排序
        PriorityQueue<AStarNode> openSet = new PriorityQueue<>(
            Comparator.comparingInt(n -> n.g + n.h)
        );
        Map<String, Integer> gScore = new HashMap<>();
        Map<String, State> parent = new HashMap<>();
        Map<String, Move> parentMove = new HashMap<>();

        int h0 = heuristic(initial, target);
        openSet.offer(new AStarNode(initial, 0, h0));
        gScore.put(initial.encoded, 0);

        while (!openSet.isEmpty()) {
            AStarNode current = openSet.poll();

            if (current.state.equals(target)) {
                return reconstructPath(parent, parentMove, initial, current.state);
            }

            int currentG = gScore.getOrDefault(current.state.encoded, Integer.MAX_VALUE);
            if (current.g > currentG) continue; // 已找到更优路径

            for (Move move : current.state.getLegalMoves()) {
                State next = current.state.applyMove(move);
                int tentativeG = current.g + 1;

                if (tentativeG < gScore.getOrDefault(next.encoded, Integer.MAX_VALUE)) {
                    parent.put(next.encoded, current.state);
                    parentMove.put(next.encoded, move);
                    gScore.put(next.encoded, tentativeG);
                    int h = heuristic(next, target);
                    openSet.offer(new AStarNode(next, tentativeG, h));
                }
            }
        }

        return null;
    }

    /**
     * 启发函数:计算错位杯子数量
     * 每个不在正确位置的杯子至少需要一次移动
     */
    private int heuristic(State state, State target) {
        // 简单启发:统计不在目标列的杯子数
        int misplaced = 0;

        // 构建目标位置映射:杯子编号 -> 目标列索引
        Map<Integer, Integer> targetPos = new HashMap<>();
        for (int col = 0; col < target.columns.size(); col++) {
            for (int cup : target.columns.get(col)) {
                targetPos.put(cup, col);
            }
        }

        for (int col = 0; col < state.columns.size(); col++) {
            for (int cup : state.columns.get(col)) {
                if (targetPos.getOrDefault(cup, -1) != col) {
                    misplaced++;
                }
            }
        }

        return misplaced / 2; // 每次移动最多修正2个错位
    }

    static class AStarNode {
        final State state;
        final int g; // 实际代价
        final int h; // 启发估计

        AStarNode(State state, int g, int h) {
            this.state = state;
            this.g = g;
            this.h = h;
        }
    }

    /**
     * 回溯重建操作序列
     */
    private List<Move> reconstructPath(Map<String, State> parent, 
                                        Map<String, Move> parentMove,
                                        State start, State end) {
        LinkedList<Move> path = new LinkedList<>();
        State current = end;

        while (!current.equals(start)) {
            Move move = parentMove.get(current.encoded);
            path.addFirst(move);
            current = parent.get(current.encoded);
        }

        return path;
    }

    /**
     * 创建示例初始状态和目标状态
     * 杯子编号 1-N,目标状态为所有杯子按顺序叠在第一列
     */
    public static void demo() {
        // 示例:4个杯子,3列
        // 初始:列0=[1,2], 列1=[3], 列2=[4]
        // 目标:列0=[1,2,3,4], 列1=[], 列2=[]

        Stack<Integer> col0 = new Stack<>();
        col0.push(1);
        col0.push(2);

        Stack<Integer> col1 = new Stack<>();
        col1.push(3);

        Stack<Integer> col2 = new Stack<>();
        col2.push(4);

        List<Stack<Integer>> initialCols = Arrays.asList(col0, col1, col2);
        State initial = new State(initialCols);

        Stack<Integer> t0 = new Stack<>();
        t0.push(1);
        t0.push(2);
        t0.push(3);
        t0.push(4);

        List<Stack<Integer>> targetCols = Arrays.asList(t0, new Stack<>(), new Stack<>());
        State target = new State(targetCols);

        CupStacking game = new CupStacking(4, 3);

        System.out.println("=== 初始状态 ===");
        System.out.println(initial);
        System.out.println("=== 目标状态 ===");
        System.out.println(target);

        // BFS求解
        System.out.println("=== BFS求解 ===");
        long startTime = System.currentTimeMillis();
        List<Move> bfsSolution = game.solveBFS(initial, target);
        long bfsTime = System.currentTimeMillis() - startTime;

        if (bfsSolution != null) {
            System.out.printf("BFS找到最优解,步数:%d,耗时:%dms%n", 
                bfsSolution.size(), bfsTime);
            for (int i = 0; i < bfsSolution.size(); i++) {
                System.out.printf("  第%d步: %s%n", i + 1, bfsSolution.get(i));
            }
        }

        // A*求解
        System.out.println("\n=== A*求解 ===");
        startTime = System.currentTimeMillis();
        List<Move> aStarSolution = game.solveAStar(initial, target);
        long aStarTime = System.currentTimeMillis() - startTime;

        if (aStarSolution != null) {
            System.out.printf("A*找到最优解,步数:%d,耗时:%dms%n", 
                aStarSolution.size(), aStarTime);
            for (int i = 0; i < aStarSolution.size(); i++) {
                System.out.printf("  第%d步: %s%n", i + 1, aStarSolution.get(i));
            }
        }
    }

    public static void main(String[] args) {
        demo();
    }
}

算法复杂度分析

算法 时间复杂度 空间复杂度 最优性 适用场景
BFS O(b^d) O(b^d) 保证最优 状态空间小
A* O(b^d) 最坏,O(d) 平均 O(b^d) 保证最优(可采纳启发式) 状态空间大
DFS O(b^d) O(d) 不保证最优 仅需任意解

其中 b 为分支因子(合法移动数),d 为最优解深度。

扩展思考

  1. IDA*算法:当内存受限时,迭代加深 A* 以时间换空间,仅需 O(d) 内存。
  2. 双向搜索:从初始状态和目标状态同时 BFS,可将复杂度从 O(b^d) 降至 O(b^(d/2))。
  3. Pattern Database:预计算子问题的距离作为更强启发式,显著提升 A* 效率。
  4. 增强约束:实际叠叠杯可能有额外规则(如只能移顶部、列高度限制),需在 getLegalMoves 中增加约束判断。

总结

叠叠杯问题完美诠释了状态空间搜索的核心思想。BFS 提供了完备性和最优性保证,而 A* 通过启发式引导搜索方向,在复杂问题中实现了数量级的加速。这种”状态表示 → 转移规则 → 搜索策略”的三段式建模方法,可广泛应用于魔方求解、华容道、推箱子等经典益智游戏。