每日算法 — 使用java实现蛇梯棋:最短路径BFS与马尔可夫链概率分析

游戏背景与问题建模

蛇梯棋(Snakes and Ladders)是最古老的棋盘游戏之一,起源于印度。玩家掷骰子沿编号格子前进,遇到梯子(底部数字小,顶部数字大)则向上攀升,遇到蛇头(数字大)则滑落回蛇尾(数字小)。

本文用Java实现蛇梯棋的完整分析系统,包括:最短路径计算(将蛇梯视为有向图的边)、马尔可夫链稳态分析、以及多玩家获胜概率模拟。

核心算法设计

1. 有向图建模

将棋盘建模为有向图:
– 顶点:0到N(N为终点)
– 边:从 $i$ 到 $i+k$($k$ 为骰子点数,$1 \le k \le M$)
– 蛇/梯子作为”传送边”:从蛇头到蛇尾、从梯底到梯顶

2. 最短路径(BFS变种)

由于骰子每次可前进1~M格,最短路径问题转化为:最少需要多少次掷骰子才能到达终点?

使用BFS求解:从起点开始,每次扩展 $M$ 个状态($i+1$ 到 $i+M$),遇到蛇/梯子直接传送。第一个到达终点的路径即为最优解。

3. 精确期望步数(动态规划)

设 $E[i]$ 为从格子 $i$ 到达终点的期望步数:

$$E[i] = 1 + \frac{1}{M} \sum_{k=1}^{M} E[next(i+k)]$$

通过高斯-赛德尔迭代求解此线性方程组。

4. 多玩家蒙特卡洛模拟

模拟大量对局,统计各玩家获胜概率及游戏平均时长。

Java实现

package snakesladders;

import java.util.*;

/**
 * 蛇梯棋分析系统
 * 核心算法:BFS最短路径 + 马尔可夫链期望计算 + 蒙特卡洛模拟
 */
public class SnakesLaddersAnalyzer {

    private final int boardSize;       // 棋盘大小(100格)
    private final int diceFaces;       // 骰子面数(6面)
    private final Map<Integer, Integer> snakes;   // 蛇头 -> 蛇尾
    private final Map<Integer, Integer> ladders;  // 梯底 -> 梯顶

    public SnakesLaddersAnalyzer(int boardSize, int diceFaces) {
        this.boardSize = boardSize;
        this.diceFaces = diceFaces;
        this.snakes = new HashMap<>();
        this.ladders = new HashMap<>();
    }

    public void addSnake(int head, int tail) {
        if (head > tail) snakes.put(head, tail);
    }

    public void addLadder(int bottom, int top) {
        if (top > bottom) ladders.put(bottom, top);
    }

    /**
     * 计算经过蛇或梯子后的实际位置
     */
    private int getActualPosition(int pos) {
        if (pos > boardSize) return boardSize;
        if (snakes.containsKey(pos)) return snakes.get(pos);
        if (ladders.containsKey(pos)) return ladders.get(pos);
        return pos;
    }

    /**
     * BFS计算最短路径(最少掷骰子次数)
     * @return [最少步数, 路径列表]
     */
    public Object[] findShortestPath() {
        Queue<int[]> queue = new LinkedList<>();  // [位置, 步数]
        boolean[] visited = new boolean[boardSize + 1];
        int[] parent = new int[boardSize + 1];    // 记录路径
        int[] moveUsed = new int[boardSize + 1];  // 记录使用的骰子点数

        queue.offer(new int[]{0, 0});
        visited[0] = true;
        parent[0] = -1;

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();
            int pos = cur[0], steps = cur[1];

            if (pos == boardSize) {
                // 重建路径
                List<Integer> path = new ArrayList<>();
                List<Integer> moves = new ArrayList<>();
                int p = pos;
                while (p != -1) {
                    path.add(p);
                    if (parent[p] != -1) moves.add(moveUsed[p]);
                    p = parent[p];
                }
                Collections.reverse(path);
                Collections.reverse(moves);
                return new Object[]{steps, path, moves};
            }

            // 尝试所有骰子点数
            for (int dice = 1; dice <= diceFaces; dice++) {
                int nextPos = getActualPosition(pos + dice);
                if (nextPos <= boardSize && !visited[nextPos]) {
                    visited[nextPos] = true;
                    parent[nextPos] = pos;
                    moveUsed[nextPos] = dice;
                    queue.offer(new int[]{nextPos, steps + 1});
                }
            }
        }

        return null;  // 无法到达(理论上不会发生)
    }

    /**
     * 动态规划迭代计算期望步数
     */
    public double[] computeExpectedSteps() {
        double[] E = new double[boardSize + 1];
        E[boardSize] = 0;

        double epsilon = 1e-10;
        for (int iter = 0; iter < 100000; iter++) {
            double maxDiff = 0;

            for (int i = boardSize - 1; i >= 0; i--) {
                double oldVal = E[i];
                double sum = 0;
                int valid = 0;

                for (int k = 1; k <= diceFaces; k++) {
                    int next = getActualPosition(i + k);
                    if (next <= boardSize) {
                        sum += E[next];
                        valid++;
                    }
                }

                if (valid > 0) {
                    E[i] = 1.0 + sum / valid;
                }

                maxDiff = Math.max(maxDiff, Math.abs(E[i] - oldVal));
            }

            if (maxDiff < epsilon) {
                System.out.println("期望步数收敛于第 " + (iter + 1) + " 次迭代");
                break;
            }
        }

        return E;
    }

    /**
     * 计算经过n步后位于各格子的精确概率分布
     */
    public double[][] computeStepDistribution(int maxSteps) {
        double[][] dp = new double[maxSteps + 1][boardSize + 1];
        dp[0][0] = 1.0;

        for (int step = 1; step <= maxSteps; step++) {
            for (int pos = 0; pos < boardSize; pos++) {
                if (dp[step - 1][pos] == 0) continue;

                for (int dice = 1; dice <= diceFaces; dice++) {
                    int next = getActualPosition(pos + dice);
                    if (next <= boardSize) {
                        dp[step][next] += dp[step - 1][pos] / diceFaces;
                    }
                }
            }
        }

        return dp;
    }

    /**
     * 蒙特卡洛模拟多玩家对局
     * @param numPlayers 玩家数量
     * @param numGames 模拟局数
     * @return 各玩家获胜概率和平均游戏长度
     */
    public SimulationResult simulateMultiplayer(int numPlayers, int numGames) {
        Random rand = new Random(42);
        int[] wins = new int[numPlayers];
        int totalTurns = 0;
        int minTurns = Integer.MAX_VALUE;
        int maxTurns = 0;

        for (int game = 0; game < numGames; game++) {
            int[] positions = new int[numPlayers];
            boolean[] finished = new boolean[numPlayers];
            int turns = 0;
            int remaining = numPlayers;

            while (remaining > 0) {
                turns++;
                for (int p = 0; p < numPlayers; p++) {
                    if (finished[p]) continue;

                    int roll = rand.nextInt(diceFaces) + 1;
                    positions[p] = getActualPosition(positions[p] + roll);

                    if (positions[p] >= boardSize) {
                        positions[p] = boardSize;
                        if (!finished[p]) {
                            finished[p] = true;
                            remaining--;
                            if (remaining == numPlayers - 1) {
                                wins[p]++;  // 第一名
                            }
                        }
                    }
                }
            }

            totalTurns += turns;
            minTurns = Math.min(minTurns, turns);
            maxTurns = Math.max(maxTurns, turns);
        }

        SimulationResult result = new SimulationResult();
        result.winProbabilities = new double[numPlayers];
        for (int p = 0; p < numPlayers; p++) {
            result.winProbabilities[p] = (double) wins[p] / numGames;
        }
        result.averageTurns = (double) totalTurns / numGames;
        result.minTurns = minTurns;
        result.maxTurns = maxTurns;

        return result;
    }

    static class SimulationResult {
        double[] winProbabilities;
        double averageTurns;
        int minTurns, maxTurns;
    }

    /**
     * 分析每条蛇和梯子的"价值":对期望步数的影响
     */
    public void analyzeSnakesAndLaddersValue() {
        System.out.println("\n=== 蛇和梯子的影响分析 ===");

        double[] baseExpected = computeExpectedSteps();
        double base = baseExpected[0];

        System.out.println("\n梯子价值(缩短期望步数):");
        List<Map.Entry<Integer, Integer>> ladderList = new ArrayList<>(ladders.entrySet());
        ladderList.sort(Map.Entry.comparingByKey());

        for (Map.Entry<Integer, Integer> entry : ladderList) {
            int bottom = entry.getKey(), top = entry.getValue();
            int gain = top - bottom;
            System.out.printf("  梯子 %d -> %d: 攀升 %d 格%n", bottom, top, gain);
        }

        System.out.println("\n蛇的危险度(增加期望步数):");
        List<Map.Entry<Integer, Integer>> snakeList = new ArrayList<>(snakes.entrySet());
        snakeList.sort((a, b) -> b.getKey() - a.getKey());  // 按蛇头位置降序

        for (Map.Entry<Integer, Integer> entry : snakeList) {
            int head = entry.getKey(), tail = entry.getValue();
            int loss = head - tail;
            System.out.printf("  蛇 %d -> %d: 滑落 %d 格%n", head, tail, loss);
        }

        System.out.printf("\n从起点到终点的期望步数: %.2f%n", base);
        System.out.printf("最短路径步数: %d%n", findShortestPath()[0]);
    }

    /**
     * 生成完整分析报告
     */
    public void generateReport() {
        System.out.println("========== 蛇梯棋完整分析报告 ==========\n");

        // 最短路径
        Object[] shortest = findShortestPath();
        @SuppressWarnings("unchecked")
        List<Integer> path = (List<Integer>) shortest[1];
        @SuppressWarnings("unchecked")
        List<Integer> moves = (List<Integer>) shortest[2];
        System.out.println("最短路径(" + shortest[0] + " 步):");
        System.out.println("  路径: " + path);
        System.out.println("  骰子序列: " + moves);

        // 期望步数
        double[] expected = computeExpectedSteps();
        System.out.printf("\n从起点到达终点的期望步数: %.4f%n", expected[0]);

        // 步数分布
        System.out.println("\n前15步到达终点的概率:");
        double[][] dist = computeStepDistribution(15);
        for (int s = 1; s <= 15; s++) {
            System.out.printf("  第 %2d 步: %.8f%n", s, dist[s][boardSize]);
        }

        // 多玩家模拟
        System.out.println("\n2人游戏获胜概率(10万次模拟):");
        SimulationResult result = simulateMultiplayer(2, 100000);
        for (int p = 0; p < result.winProbabilities.length; p++) {
            System.out.printf("  玩家 %d: %.4f%n", p + 1, result.winProbabilities[p]);
        }
        System.out.printf("平均游戏长度: %.2f 回合%n", result.averageTurns);
        System.out.printf("最短/最长游戏: %d / %d 回合%n", result.minTurns, result.maxTurns);

        // 蛇梯分析
        analyzeSnakesAndLaddersValue();
    }

    public static void main(String[] args) {
        // 创建经典100格蛇梯棋
        SnakesLaddersAnalyzer game = new SnakesLaddersAnalyzer(100, 6);

        // 经典蛇布局
        game.addSnake(99, 1);   game.addSnake(95, 35);
        game.addSnake(92, 48);  game.addSnake(89, 51);
        game.addSnake(74, 42);  game.addSnake(64, 22);
        game.addSnake(46, 13);  game.addSnake(33, 9);

        // 经典梯子布局
        game.addLadder(3, 22);  game.addLadder(8, 26);
        game.addLadder(20, 41); game.addLadder(28, 56);
        game.addLadder(40, 59); game.addLadder(50, 67);
        game.addLadder(63, 81); game.addLadder(71, 91);

        game.generateReport();
    }
}

算法复杂度分析

操作 时间复杂度 空间复杂度
BFS最短路径 $O(N \cdot M)$ $O(N)$
期望步数(迭代DP) $O(I \cdot N \cdot M)$,$I$ 为迭代次数 $O(N)$
概率分布 $O(S \cdot N \cdot M)$,$S$ 为步数 $O(S \cdot N)$
蒙特卡洛模拟 $O(K \cdot N \cdot M)$,$K$ 为模拟次数 $O(N)$

扩展思考

  1. 最优骰子设计:若可自定义骰子点数分布,如何最小化期望步数?
  2. 动态蛇梯:蛇和梯子位置随游戏进程变化,增加策略深度。
  3. 技能卡机制:加入道具系统(如”跳过蛇”、”双倍移动”),可用强化学习训练最优使用策略。

总结

本文将蛇梯棋抽象为有向图上的最短路径与马尔可夫链问题,使用Java实现了BFS最优路径搜索、期望步数动态规划计算和多玩家蒙特卡洛模拟。算法展示了图论、概率论与动态规划在经典桌游分析中的综合应用。