每日算法 — 使用java实现扫雷:高斯消元与贝叶斯条件概率推理

引言

扫雷是一款经典的逻辑推理游戏。玩家在揭开格子的同时,需要根据已显示的数字推断周围地雷的分布。本文将介绍一种进阶解法:利用高斯消元将扫雷的约束条件转化为线性方程组,再通过贝叶斯推理计算每个未揭示格子是地雷的概率,从而做出最优决策。

核心算法原理

1. 约束建模

扫雷中每个已揭示的数字格,其周围未揭示的格子构成一个约束:这些格子中地雷的总数等于该数字。若用变量 $x_i \in {0,1}$ 表示第 $i$ 个未揭示格子是否为地雷,则每个数字格对应一个线性方程:

$$\sum_{i \in \text{邻居}} x_i = \text{数字}$$

将所有约束收集起来,就得到一个定义在 $\mathbb{F}_2$(二元域)上的线性方程组。注意这里的等号实际上是模2等号,但由于右侧是具体的计数值而非模2值,更准确的处理是在实数域上进行高斯消元,然后结合整数约束求解。

2. 高斯消元求解

将约束写成增广矩阵形式,对实数矩阵执行高斯消元。消元后,部分变量可以被唯一确定(主元变量),而另一部分则是自由变量。对于每个自由变量的赋值组合,检查是否满足所有约束(右侧数字匹配),即可枚举出所有合法的地雷分布方案。

3. 贝叶斯概率计算

枚举出所有合法方案后,某个格子是地雷的概率为:

$$P(\text{地雷}) = \frac{\text{该格子为地雷的方案数}}{\text{总合法方案数}}$$

对于与任何数字格都不相邻的孤立区域,需要结合剩余地雷数进行全局概率修正。

Java实现

import java.util.*;

/**
 * 扫雷高斯消元求解器
 * 将数字约束转化为线性方程组,枚举所有合法解后计算贝叶斯概率
 */
public class MinesweeperGaussianSolver {

    // 扫雷棋盘状态: -1=未揭示, 0-8=已揭示的数字, -2=已标记地雷
    private int[][] board;
    private int rows, cols;
    private int totalMines; // 总地雷数

    public MinesweeperGaussianSolver(int[][] board, int totalMines) {
        this.board = board;
        this.rows = board.length;
        this.cols = board[0].length;
        this.totalMines = totalMines;
    }

    /**
     * 收集所有约束:每个数字格对应一个方程
     * 返回形式: List<int[]> 每个int[]长度为(变量数+1),最后一位是常数项
     */
    private List<int[]> collectConstraints(List<int[]> variables) {
        List<int[]> equations = new ArrayList<>();

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (board[r][c] >= 0) { // 已揭示的数字格
                    int num = board[r][c];
                    int[] eq = new int[variables.size() + 1];
                    int count = 0;

                    // 遍历8邻域
                    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 (nr >= 0 && nr < rows && nc >= 0 && nc < cols 
                                && board[nr][nc] == -1) {
                                // 找到该变量在列表中的索引
                                for (int i = 0; i < variables.size(); i++) {
                                    if (variables.get(i)[0] == nr && variables.get(i)[1] == nc) {
                                        eq[i] = 1;
                                        count++;
                                        break;
                                    }
                                }
                            }
                        }
                    }

                    // 减去已标记的地雷数
                    int flagged = countFlaggedNeighbors(r, c);
                    eq[variables.size()] = num - flagged;

                    if (count > 0) {
                        equations.add(eq);
                    }
                }
            }
        }
        return equations;
    }

    private int countFlaggedNeighbors(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 (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc] == -2) {
                    count++;
                }
            }
        }
        return count;
    }

    /**
     * 高斯消元(实数域)
     * 返回行阶梯形矩阵
     */
    private double[][] gaussianEliminate(double[][] matrix, int varCount) {
        int rowCount = matrix.length;
        if (rowCount == 0) return matrix;
        int colCount = varCount + 1;

        int row = 0;
        for (int col = 0; col < varCount && row < rowCount; col++) {
            // 找主元
            int pivot = row;
            for (int i = row; i < rowCount; i++) {
                if (Math.abs(matrix[i][col]) > Math.abs(matrix[pivot][col])) {
                    pivot = i;
                }
            }

            if (Math.abs(matrix[pivot][col]) < 1e-9) continue;

            // 交换行
            double[] temp = matrix[row];
            matrix[row] = matrix[pivot];
            matrix[pivot] = temp;

            // 归一化
            double div = matrix[row][col];
            for (int j = col; j < colCount; j++) {
                matrix[row][j] /= div;
            }

            // 消去下方行
            for (int i = row + 1; i < rowCount; i++) {
                double factor = matrix[i][col];
                if (Math.abs(factor) > 1e-9) {
                    for (int j = col; j < colCount; j++) {
                        matrix[i][j] -= factor * matrix[row][j];
                    }
                }
            }
            row++;
        }
        return matrix;
    }

    /**
     * 枚举所有合法解,计算每个变量的地雷概率
     */
    public Map<int[], Double> solve() {
        // 收集所有未揭示且未标记的变量
        List<int[]> variables = new ArrayList<>();
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (board[r][c] == -1) {
                    variables.add(new int[]{r, c});
                }
            }
        }

        if (variables.isEmpty()) return new HashMap<>();

        List<int[]> constraints = collectConstraints(variables);
        int varCount = variables.size();

        // 转为double矩阵进行高斯消元
        double[][] matrix = new double[constraints.size()][varCount + 1];
        for (int i = 0; i < constraints.size(); i++) {
            for (int j = 0; j <= varCount; j++) {
                matrix[i][j] = constraints.get(i)[j];
            }
        }

        double[][] reduced = gaussianEliminate(matrix, varCount);

        // 识别主元列和自由变量
        boolean[] isPivot = new boolean[varCount];
        int[] pivotRow = new int[varCount];
        Arrays.fill(pivotRow, -1);

        int r = 0;
        for (int c = 0; c < varCount && r < reduced.length; c++) {
            if (r < reduced.length && Math.abs(reduced[r][c]) > 0.5) {
                isPivot[c] = true;
                pivotRow[c] = r;
                r++;
            }
        }

        // 枚举自由变量(使用DFS/回溯)
        List<boolean[]> validSolutions = new ArrayList<>();
        boolean[] assignment = new boolean[varCount];
        enumerateSolutions(reduced, isPivot, pivotRow, varCount, 0, assignment, validSolutions);

        // 计算贝叶斯概率
        Map<int[], Double> probabilities = new HashMap<>();
        if (validSolutions.isEmpty()) return probabilities;

        for (int i = 0; i < varCount; i++) {
            int mineCount = 0;
            for (boolean[] sol : validSolutions) {
                if (sol[i]) mineCount++;
            }
            double prob = (double) mineCount / validSolutions.size();
            probabilities.put(variables.get(i), prob);
        }

        return probabilities;
    }

    private void enumerateSolutions(double[][] matrix, boolean[] isPivot, int[] pivotRow,
                                     int varCount, int varIdx, boolean[] assignment,
                                     List<boolean[]> validSolutions) {
        if (varIdx == varCount) {
            // 验证所有方程
            if (verifySolution(matrix, assignment, varCount)) {
                validSolutions.add(assignment.clone());
            }
            return;
        }

        if (isPivot[varIdx]) {
            // 主元变量由对应行计算得出
            int row = pivotRow[varIdx];
            double value = matrix[row][varCount]; // 常数项
            for (int c = varIdx + 1; c < varCount; c++) {
                if (Math.abs(matrix[row][c]) > 1e-9) {
                    value -= matrix[row][c] * (assignment[c] ? 1 : 0);
                }
            }
            // 主元变量必须取整数值0或1
            if (Math.abs(value) < 1e-9) {
                assignment[varIdx] = false;
                enumerateSolutions(matrix, isPivot, pivotRow, varCount, varIdx + 1, assignment, validSolutions);
            } else if (Math.abs(value - 1) < 1e-9) {
                assignment[varIdx] = true;
                enumerateSolutions(matrix, isPivot, pivotRow, varCount, varIdx + 1, assignment, validSolutions);
            }
            // 其他值表示无解分支
        } else {
            // 自由变量:尝试0和1
            assignment[varIdx] = false;
            enumerateSolutions(matrix, isPivot, pivotRow, varCount, varIdx + 1, assignment, validSolutions);
            assignment[varIdx] = true;
            enumerateSolutions(matrix, isPivot, pivotRow, varCount, varIdx + 1, assignment, validSolutions);
        }
    }

    private boolean verifySolution(double[][] matrix, boolean[] assignment, int varCount) {
        for (double[] row : matrix) {
            double sum = 0;
            for (int c = 0; c < varCount; c++) {
                if (Math.abs(row[c]) > 1e-9) {
                    sum += row[c] * (assignment[c] ? 1 : 0);
                }
            }
            double rhs = row[varCount];
            if (Math.abs(sum - rhs) > 1e-6) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        // 示例: 3x3棋盘,中心为1,其余未揭示
        int[][] board = {
            {-1, -1, -1},
            {-1,  1, -1},
            {-1, -1, -1}
        };

        MinesweeperGaussianSolver solver = new MinesweeperGaussianSolver(board, 1);
        Map<int[], Double> probs = solver.solve();

        System.out.println("各格子地雷概率:");
        for (Map.Entry<int[], Double> entry : probs.entrySet()) {
            int[] pos = entry.getKey();
            System.out.printf("(%d,%d): %.2f%%\n", pos[0], pos[1], entry.getValue() * 100);
        }
    }
}

复杂度分析

  • 约束收集:$O(R \times C \times 8)$,即线性扫描整个棋盘。
  • 高斯消元:设变量数为 $V$,约束数为 $E$,时间复杂度为 $O(E \times V^2)$。
  • 解枚举:自由变量数为 $F$,最坏情况 $O(2^F \times E \times V)$。实际扫雷中,通过边界分离可将 $F$ 控制在较小范围。
  • 空间复杂度:$O(E \times V)$,用于存储增广矩阵。

总结

本文将扫雷的推理问题转化为线性代数问题:通过高斯消元识别确定变量和自由变量,枚举所有合法配置后利用贝叶斯公式计算概率。该方法在高级扫雷求解器中被广泛使用,尤其在局面复杂、纯逻辑推理无法推进时,概率计算能给出最优点击策略。