每日算法 — 使用java实现扫雷:贝叶斯推理与条件概率安全格判定

引言

扫雷是一款考验逻辑推理的经典游戏。传统解法通常依赖确定性规则(如相邻数字与未揭开格子的精确匹配),但在复杂局面下往往面临信息不足、无法确定安全格的困境。本文引入贝叶斯推理框架,将扫雷的每一步决策建模为概率推断问题:在已知数字约束的条件下,计算每个未揭开格子是地雷的后验概率,从而做出期望损失最小的决策。

问题建模

状态定义

将扫雷地图划分为三类格子:
– 已揭开数字格:提供邻接地雷数量的精确约束
– 未揭开格:待推断的后验概率分布
– 已标记雷格:确定是地雷

贝叶斯网络结构

每个已揭开的数字格是一个约束节点,其值等于周围8格中地雷数量的总和。所有未揭开格是二值隐变量(是雷/非雷)。整个地图构成一个因子图(Factor Graph):数字格作为因子节点,未揭开格作为变量节点,边表示邻接关系。

推理目标

对每个未揭开格子c,计算:

P(c = 地雷 | 所有已揭开数字) = ?

当精确推理因组合爆炸不可行时,采用信念传播(Belief Propagation)的近似推断方法。

核心算法设计

1. 约束提取与分组

将相互关联的未揭开格子和数字约束划分为独立组件,降低推理复杂度。若两个数字格共享未揭开邻居,则属于同一组件。

2. 精确枚举(小组件)

对于未揭开格子数n ≤ 20的组件,直接枚举所有2^n种地雷配置,筛选满足所有数字约束的合法配置,统计每个格子在合法配置中为地雷的频率作为后验概率。

3. 边缘概率计算(大组件)

对于大组件,采用以下策略:
– 确定性推断优先:如果某数字格的剩余未揭开邻居数恰好等于剩余雷数,则全部标记为雷;如果剩余雷数为0,则全部安全
– 安全格优先:选择后验概率最小的未揭开格子揭开
– 最小风险决策:当所有未揭开格概率相近时,优先选择邻接已揭开数字最少的格子(信息增益最大)

Java完整实现

import java.util.*;

/**
 * 基于贝叶斯推理的扫雷AI
 * 使用条件概率计算安全格,实现期望损失最小化决策
 */
public class BayesianMinesweeper {

    // 地图状态
    private final int rows, cols, totalMines;
    private Cell[][] board;
    private int remainingMines;
    private int unrevealedCount;
    private final Random random = new Random();

    // 单元格状态
    enum CellState { UNREVEALED, REVEALED, FLAGGED }

    static class Cell {
        CellState state = CellState.UNREVEALED;
        boolean isMine = false;
        int adjacentMines = 0; // 周围地雷数(仅REVEALED时有效)
        double probMine = 0.5; // 是雷的后验概率
    }

    public BayesianMinesweeper(int rows, int cols, int mines) {
        this.rows = rows;
        this.cols = cols;
        this.totalMines = mines;
        this.remainingMines = mines;
        this.unrevealedCount = rows * cols;
        this.board = new Cell[rows][cols];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                board[i][j] = new Cell();
            }
        }
    }

    /**
     * 初始化地雷(确保第一次点击安全)
     */
    public void initMines(int safeRow, int safeCol) {
        List<int[]> candidates = new ArrayList<>();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (i != safeRow || j != safeCol) {
                    candidates.add(new int[]{i, j});
                }
            }
        }
        Collections.shuffle(candidates, random);
        for (int k = 0; k < totalMines && k < candidates.size(); k++) {
            int[] pos = candidates.get(k);
            board[pos[0]][pos[1]].isMine = true;
        }

        // 计算每个格子的邻接地雷数
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (!board[i][j].isMine) {
                    board[i][j].adjacentMines = countAdjacentMines(i, j);
                }
            }
        }
    }

    private int countAdjacentMines(int r, int c) {
        int count = 0;
        for (int dr = -1; dr <= 1; dr++) {
            for (int dc = -1; dc <= 1; dc++) {
                if (dr == 0 && dc == 0) continue;
                int nr = r + dr, nc = c + dc;
                if (inBounds(nr, nc) && board[nr][nc].isMine) count++;
            }
        }
        return count;
    }

    private boolean inBounds(int r, int c) {
        return r >= 0 && r < rows && c >= 0 && c < cols;
    }

    /**
     * 揭开格子,返回是否触发地雷
     */
    public boolean reveal(int r, int c) {
        Cell cell = board[r][c];
        if (cell.state != CellState.UNREVEALED) return false;

        cell.state = CellState.REVEALED;
        unrevealedCount--;

        if (cell.isMine) return true; // 踩雷

        // 如果是0,自动揭开周围(级联展开)
        if (cell.adjacentMines == 0) {
            for (int dr = -1; dr <= 1; dr++) {
                for (int dc = -1; dc <= 1; dc++) {
                    if (dr == 0 && dc == 0) continue;
                    int nr = r + dr, nc = c + dc;
                    if (inBounds(nr, nc)) reveal(nr, nc);
                }
            }
        }
        return false;
    }

    /**
     * 标记/取消标记地雷
     */
    public void flag(int r, int c) {
        Cell cell = board[r][c];
        if (cell.state == CellState.UNREVEALED) {
            cell.state = CellState.FLAGGED;
            remainingMines--;
        } else if (cell.state == CellState.FLAGGED) {
            cell.state = CellState.UNREVEALED;
            remainingMines++;
        }
    }

    /**
     * 获取邻居列表
     */
    private List<int[]> getNeighbors(int r, int c) {
        List<int[]> list = new ArrayList<>();
        for (int dr = -1; dr <= 1; dr++) {
            for (int dc = -1; dc <= 1; dc++) {
                if (dr == 0 && dc == 0) continue;
                int nr = r + dr, nc = c + dc;
                if (inBounds(nr, nc)) list.add(new int[]{nr, nc});
            }
        }
        return list;
    }

    /**
     * 提取所有约束组件
     * 每个约束为:(数字格位置, 相邻未揭开格子列表, 剩余地雷数)
     */
    private List<Constraint> extractConstraints() {
        List<Constraint> constraints = new ArrayList<>();

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                Cell cell = board[i][j];
                if (cell.state != CellState.REVEALED) continue;

                List<int[]> neighbors = getNeighbors(i, j);
                List<int[]> unrevealed = new ArrayList<>();
                int flaggedCount = 0;

                for (int[] n : neighbors) {
                    Cell nc = board[n[0]][n[1]];
                    if (nc.state == CellState.UNREVEALED) {
                        unrevealed.add(n);
                    } else if (nc.state == CellState.FLAGGED) {
                        flaggedCount++;
                    }
                }

                int remaining = cell.adjacentMines - flaggedCount;
                if (remaining >= 0 && !unrevealed.isEmpty()) {
                    constraints.add(new Constraint(i, j, unrevealed, remaining));
                }
            }
        }
        return constraints;
    }

    static class Constraint {
        int row, col;
        List<int[]> cells;
        int remainingMines;
        Constraint(int r, int c, List<int[]> cells, int rem) {
            this.row = r; this.col = c; this.cells = cells; this.remainingMines = rem;
        }
    }

    /**
     * 将约束划分为连通组件
     */
    private List<List<Constraint>> splitComponents(List<Constraint> constraints) {
        if (constraints.isEmpty()) return new ArrayList<>();

        // 构建邻接图:共享未揭开格子的约束相连
        int n = constraints.size();
        boolean[][] adj = new boolean[n][n];

        for (int i = 0; i < n; i++) {
            Set<String> cellsI = new HashSet<>();
            for (int[] c : constraints.get(i).cells) cellsI.add(c[0] + "," + c[1]);

            for (int j = i + 1; j < n; j++) {
                for (int[] c : constraints.get(j).cells) {
                    if (cellsI.contains(c[0] + "," + c[1])) {
                        adj[i][j] = adj[j][i] = true;
                        break;
                    }
                }
            }
        }

        boolean[] vis = new boolean[n];
        List<List<Constraint>> components = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            if (vis[i]) continue;
            List<Constraint> comp = new ArrayList<>();
            Queue<Integer> q = new LinkedList<>();
            q.add(i);
            vis[i] = true;

            while (!q.isEmpty()) {
                int u = q.poll();
                comp.add(constraints.get(u));
                for (int v = 0; v < n; v++) {
                    if (adj[u][v] && !vis[v]) {
                        vis[v] = true;
                        q.add(v);
                    }
                }
            }
            components.add(comp);
        }
        return components;
    }

    /**
     * 对小组件进行精确枚举,计算每个格子的地雷概率
     */
    private Map<String, Double> enumerateComponent(List<Constraint> comp) {
        Set<String> vars = new HashSet<>();
        for (Constraint c : comp) {
            for (int[] cell : c.cells) vars.add(cell[0] + "," + cell[1]);
        }

        List<String> varList = new ArrayList<>(vars);
        int n = varList.size();

        Map<String, Integer> varIndex = new HashMap<>();
        for (int i = 0; i < n; i++) varIndex.put(varList.get(i), i);

        // 枚举所有2^n种配置
        long[] mineCount = new long[n]; // 每个变量为1的次数
        long validConfigs = 0;

        int totalConfigs = 1 << n;
        for (int mask = 0; mask < totalConfigs; mask++) {
            // 快速检查约束
            boolean valid = true;
            for (Constraint c : comp) {
                int count = 0;
                for (int[] cell : c.cells) {
                    int idx = varIndex.get(cell[0] + "," + cell[1]);
                    if ((mask & (1 << idx)) != 0) count++;
                }
                if (count != c.remainingMines) {
                    valid = false;
                    break;
                }
            }

            if (valid) {
                validConfigs++;
                for (int i = 0; i < n; i++) {
                    if ((mask & (1 << i)) != 0) mineCount[i]++;
                }
            }
        }

        Map<String, Double> probs = new HashMap<>();
        for (int i = 0; i < n; i++) {
            double p = validConfigs > 0 ? (double) mineCount[i] / validConfigs : 0.5;
            probs.put(varList.get(i), p);
        }
        return probs;
    }

    /**
     * 贝叶斯推理主函数:更新所有未揭开格子的地雷概率
     */
    public void updateProbabilities() {
        // 重置概率
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (board[i][j].state == CellState.UNREVEALED) {
                    board[i][j].probMine = (double) remainingMines / unrevealedCount;
                }
            }
        }

        List<Constraint> constraints = extractConstraints();
        if (constraints.isEmpty()) return;

        List<List<Constraint>> components = splitComponents(constraints);

        for (List<Constraint> comp : components) {
            Set<String> vars = new HashSet<>();
            for (Constraint c : comp) {
                for (int[] cell : c.cells) vars.add(cell[0] + "," + cell[1]);
            }

            Map<String, Double> probs;
            if (vars.size() <= 22) {
                // 精确枚举
                probs = enumerateComponent(comp);
            } else {
                // 大组件:仅做确定性推断
                probs = deterministicInference(comp);
            }

            // 更新概率
            for (Map.Entry<String, Double> e : probs.entrySet()) {
                String[] p = e.getKey().split(",");
                int r = Integer.parseInt(p[0]), c = Integer.parseInt(p[1]);
                board[r][c].probMine = e.getValue();
            }
        }
    }

    /**
     * 确定性推断:仅处理概率为0或1的情况
     */
    private Map<String, Double> deterministicInference(List<Constraint> comp) {
        Map<String, Double> probs = new HashMap<>();
        boolean changed = true;

        while (changed) {
            changed = false;
            for (Constraint c : comp) {
                List<String> unknown = new ArrayList<>();
                for (int[] cell : c.cells) {
                    String key = cell[0] + "," + cell[1];
                    if (!probs.containsKey(key) || (probs.get(key) > 0 && probs.get(key) < 1)) {
                        unknown.add(key);
                    }
                }

                int knownMines = 0;
                for (int[] cell : c.cells) {
                    String key = cell[0] + "," + cell[1];
                    if (probs.getOrDefault(key, 0.0) == 1.0) knownMines++;
                }

                int rem = c.remainingMines - knownMines;

                if (unknown.size() == rem && rem > 0) {
                    // 全部是雷
                    for (String k : unknown) {
                        probs.put(k, 1.0);
                        changed = true;
                    }
                } else if (rem == 0) {
                    // 全部安全
                    for (String k : unknown) {
                        probs.put(k, 0.0);
                        changed = true;
                    }
                }
            }
        }

        // 未推断的设为未知
        for (Constraint c : comp) {
            for (int[] cell : c.cells) {
                String key = cell[0] + "," + cell[1];
                probs.putIfAbsent(key, 0.5);
            }
        }
        return probs;
    }

    /**
     * AI决策:选择最优动作
     * 返回 [action, row, col]: action 0=reveal, 1=flag
     */
    public int[] decideAction() {
        updateProbabilities();

        // 优先标记确定是雷的格子
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (board[i][j].state == CellState.UNREVEALED && board[i][j].probMine >= 0.999) {
                    return new int[]{1, i, j};
                }
            }
        }

        // 优先揭开确定安全的格子
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (board[i][j].state == CellState.UNREVEALED && board[i][j].probMine <= 0.001) {
                    return new int[]{0, i, j};
                }
            }
        }

        // 选择概率最小的格子揭开
        double minProb = 1.1;
        int bestR = -1, bestC = -1;

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (board[i][j].state == CellState.UNREVEALED) {
                    if (board[i][j].probMine < minProb) {
                        minProb = board[i][j].probMine;
                        bestR = i;
                        bestC = j;
                    }
                }
            }
        }

        if (bestR != -1) {
            return new int[]{0, bestR, bestC};
        }

        return null;
    }

    /**
     * 打印当前地图(AI视角)
     */
    public void printBoard() {
        System.out.println("   " + "0 1 2 3 4 5 6 7 8".substring(0, cols * 2));
        for (int i = 0; i < rows; i++) {
            System.out.print(i + "  ");
            for (int j = 0; j < cols; j++) {
                Cell c = board[i][j];
                if (c.state == CellState.FLAGGED) {
                    System.out.print("F ");
                } else if (c.state == CellState.UNREVEALED) {
                    System.out.print("? ");
                } else {
                    System.out.print(c.adjacentMines + " ");
                }
            }
            System.out.println();
        }
    }

    /**
     * 打印概率地图
     */
    public void printProbabilities() {
        System.out.println("\nMine Probabilities:");
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (board[i][j].state == CellState.UNREVEALED) {
                    System.out.printf("%.2f ", board[i][j].probMine);
                } else {
                    System.out.print("---- ");
                }
            }
            System.out.println();
        }
    }

    /**
     * 运行完整游戏
     */
    public void play() {
        // 第一步随机点击中心区域
        int sr = rows / 2, sc = cols / 2;
        initMines(sr, sc);
        reveal(sr, sc);

        int steps = 0;
        while (unrevealedCount > remainingMines) {
            int[] action = decideAction();
            if (action == null) break;

            int act = action[0], r = action[1], c = action[2];

            if (act == 1) {
                flag(r, c);
                System.out.println("Step " + (++steps) + ": Flag (" + r + "," + c + ")");
            } else {
                System.out.println("Step " + (++steps) + ": Reveal (" + r + "," + c + ") prob=" 
                        + String.format("%.3f", board[r][c].probMine));
                if (reveal(r, c)) {
                    System.out.println("BOOM! Hit a mine at (" + r + "," + c + ")");
                    printBoard();
                    return;
                }
            }
        }

        System.out.println("Victory! All mines cleared.");
        printBoard();
    }

    public static void main(String[] args) {
        BayesianMinesweeper game = new BayesianMinesweeper(9, 9, 10);
        game.play();
    }
}

算法复杂度分析

模块 时间复杂度 空间复杂度
约束提取 O(rows × cols) O(rows × cols)
组件划分(BFS) O(constraints² × neighbors) O(constraints²)
精确枚举 O(2^n × constraints) O(n)
确定性推断 O(iterations × constraints) O(variables)
单步决策 O(rows × cols + 2^n) O(rows × cols)

关键设计要点

  1. 组件分解:将全局推理问题拆分为多个独立子问题,每个组件单独求解,指数级降低复杂度
  2. 精确与近似混合:小组件精确枚举保证可靠性,大组件退化为确定性推断避免爆炸
  3. 概率最小化决策:始终选择地雷概率最低的格子揭开,实现期望生存率最大化
  4. 约束传播:利用数字格的精确等式约束,快速确定概率为0或1的格子

总结

贝叶斯推理为扫雷提供了一套严谨的概率决策框架。与确定性规则法相比,它能在信息不足时给出最优概率估计,在复杂局面下显著降低踩雷风险。读者可进一步探索将精确枚举替换为马尔可夫链蒙特卡洛(MCMC)采样,以处理更大规模的组件推理。