每日算法 — 使用java实现扫雷:支持向量机核函数分类与模式识别概率推理

引言

扫雷(Minesweeper)是一款经典的逻辑推理游戏,玩家需要根据已揭示格子的数字提示,推断隐藏雷区的分布。传统的扫雷求解依赖于约束传播和概率推理,但当棋盘规模增大、约束条件变得复杂时,纯逻辑推理可能无法给出确定性的结论。本文将探索一种创新的思路:将扫雷中的局部模式识别问题建模为支持向量机(SVM)的二分类问题,利用核函数(Kernel Function)将低维的格子特征映射到高维空间,从而训练出一个能够识别”安全格”与”雷格”的分类器,并结合概率推理为不确定区域提供决策支持。

问题建模

局部特征提取

将扫雷棋盘中的一个未揭示格子及其邻域视为一个局部模式。对于每个未揭示格子,我们提取以下特征向量:

  1. 邻域数字特征:周围8个已揭示格子的数字值(0-8),未揭示或越界用特殊值-1表示
  2. 邻域状态特征:周围8个格子的状态(已揭示/未揭示/已标记)
  3. 剩余雷数比例:当前剩余雷数与总未知格数的比值
  4. 边界复杂度:该格子位于边界还是内部区域

SVM分类器设计

将每个局部模式标记为两类:
– 正类(+1):该格子是安全格(无雷)
– 负类(-1):该格子是雷格

使用高斯径向基核函数(RBF Kernel)进行非线性映射:

$$K(x_i, x_j) = \exp\left(-\gamma |x_i – x_j|^2\right)$$

RBF核能够将低维不可分的模式映射到高维空间,适合处理扫雷中复杂的局部约束模式。

Java实现

项目结构

src/
├── model/
│   ├── Cell.java          // 格子状态枚举与数据模型
│   ├── Board.java         // 扫雷棋盘管理
│   └── PatternFeature.java // 局部模式特征向量
├── svm/
│   ├── SVMClassifier.java // SVM分类器核心
│   ├── Kernel.java        // 核函数接口
│   └── RBFKernel.java     // RBF核函数实现
├── solver/
│   ├── ConstraintSolver.java // 约束传播求解器
│   └── ProbabilityEngine.java // 概率推理引擎
└── Main.java

Cell.java — 格子状态模型

package model;

/**
 * 扫雷格子状态枚举
 * UNREVEALED: 未揭示
 * REVEALED: 已揭示(显示数字)
 * FLAGGED: 已标记为雷
 * MINE: 雷(游戏结束后显示)
 */
public enum CellState {
    UNREVEALED, REVEALED, FLAGGED, MINE
}

/**
 * 单个格子数据模型
 */
public class Cell {
    private CellState state;
    private int neighborMines; // 周围雷数,-1表示该格本身是雷
    private boolean isMine;

    public Cell() {
        this.state = CellState.UNREVEALED;
        this.neighborMines = 0;
        this.isMine = false;
    }

    public CellState getState() { return state; }
    public void setState(CellState state) { this.state = state; }
    public int getNeighborMines() { return neighborMines; }
    public void setNeighborMines(int n) { this.neighborMines = n; }
    public boolean isMine() { return isMine; }
    public void setMine(boolean mine) { this.isMine = mine; }
}

PatternFeature.java — 特征向量提取

package model;

import java.util.Arrays;

/**
 * 局部模式特征向量提取器
 * 将扫雷棋盘上的一个未揭示格子及其邻域转换为一个固定维度的特征向量
 */
public class PatternFeature {
    // 特征维度:8个邻域数字 + 8个邻域状态 + 1个剩余雷比 + 1个边界标志 = 18维
    public static final int FEATURE_DIM = 18;

    private final double[] features;
    private final int row;
    private final int col;

    public PatternFeature(double[] features, int row, int col) {
        this.features = Arrays.copyOf(features, features.length);
        this.row = row;
        this.col = col;
    }

    /**
     * 从棋盘提取指定位置的特征向量
     * @param board 扫雷棋盘
     * @param row 目标行
     * @param col 目标列
     * @param remainingMines 剩余雷数
     * @return 特征向量对象
     */
    public static PatternFeature extract(Cell[][] board, int row, int col, int remainingMines) {
        int rows = board.length;
        int cols = board[0].length;
        double[] feats = new double[FEATURE_DIM];
        int idx = 0;

        // 1. 邻域数字特征(8维)
        int unknownCount = 0;
        for (int dr = -1; dr <= 1; dr++) {
            for (int dc = -1; dc <= 1; dc++) {
                if (dr == 0 && dc == 0) continue;
                int nr = row + dr, nc = col + dc;
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                    Cell cell = board[nr][nc];
                    if (cell.getState() == CellState.REVEALED) {
                        feats[idx++] = cell.getNeighborMines() / 8.0; // 归一化到[0,1]
                    } else {
                        feats[idx++] = -1.0; // 未揭示标记
                        unknownCount++;
                    }
                } else {
                    feats[idx++] = -2.0; // 越界标记
                }
            }
        }

        // 2. 邻域状态特征(8维)
        for (int dr = -1; dr <= 1; dr++) {
            for (int dc = -1; dc <= 1; dc++) {
                if (dr == 0 && dc == 0) continue;
                int nr = row + dr, nc = col + dc;
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
                    Cell cell = board[nr][nc];
                    switch (cell.getState()) {
                        case REVEALED: feats[idx++] = 0.0; break;
                        case UNREVEALED: feats[idx++] = 1.0; break;
                        case FLAGGED: feats[idx++] = 2.0; break;
                        default: feats[idx++] = 3.0;
                    }
                } else {
                    feats[idx++] = -1.0;
                }
            }
        }

        // 3. 剩余雷数比例(1维)
        int totalUnknown = 0;
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (board[r][c].getState() == CellState.UNREVEALED) totalUnknown++;
            }
        }
        feats[idx++] = totalUnknown > 0 ? (double) remainingMines / totalUnknown : 0.0;

        // 4. 边界复杂度(1维)
        feats[idx++] = (row == 0 || row == rows - 1 || col == 0 || col == cols - 1) ? 1.0 : 0.0;

        return new PatternFeature(feats, row, col);
    }

    public double[] getFeatures() { return features; }
    public int getRow() { return row; }
    public int getCol() { return col; }
}

Kernel.java & RBFKernel.java — 核函数实现

package svm;

/**
 * SVM核函数接口
 */
public interface Kernel {
    double compute(double[] x1, double[] x2);
}

/**
 * 高斯径向基核函数(RBF Kernel)
 * K(x1, x2) = exp(-gamma * ||x1 - x2||^2)
 */
public class RBFKernel implements Kernel {
    private final double gamma;

    public RBFKernel(double gamma) {
        this.gamma = gamma;
    }

    @Override
    public double compute(double[] x1, double[] x2) {
        double sum = 0.0;
        for (int i = 0; i < x1.length; i++) {
            double diff = x1[i] - x2[i];
            sum += diff * diff;
        }
        return Math.exp(-gamma * sum);
    }
}

SVMClassifier.java — SMO简化版SVM分类器

package svm;

import model.PatternFeature;
import java.util.*;

/**
 * 基于简化SMO(Sequential Minimal Optimization)算法的SVM分类器
 * 用于扫雷局部模式的二分类(安全格 vs 雷格)
 */
public class SVMClassifier {
    private final Kernel kernel;
    private final double C;          // 惩罚参数
    private final double tol;        // 收敛容差
    private final double epsilon;    // 数值精度

    private double[] alphas;         // 拉格朗日乘子
    private double b;                // 偏置项
    private List<PatternFeature> supportVectors; // 支持向量
    private List<Double> supportLabels;          // 支持向量标签
    private List<Double> supportAlphas;          // 支持向量对应的alpha

    public SVMClassifier(Kernel kernel, double C, double tol) {
        this.kernel = kernel;
        this.C = C;
        this.tol = tol;
        this.epsilon = 1e-5;
        this.b = 0.0;
    }

    /**
     * 训练SVM模型
     * @param features 特征向量列表
     * @param labels   标签列表(+1.0表示安全格,-1.0表示雷格)
     */
    public void train(List<PatternFeature> features, List<Double> labels) {
        int n = features.size();
        alphas = new double[n];
        double[] errors = new double[n];

        // 预计算核矩阵
        double[][] K = new double[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                K[i][j] = kernel.compute(features.get(i).getFeatures(), features.get(j).getFeatures());
            }
        }

        int passes = 0;
        int maxPasses = 100;
        while (passes < maxPasses) {
            int numChanged = 0;
            for (int i = 0; i < n; i++) {
                errors[i] = computeError(features, labels, K, i);

                if ((labels.get(i) * errors[i] < -tol && alphas[i] < C) ||
                    (labels.get(i) * errors[i] > tol && alphas[i] > 0)) {

                    int j = selectSecondAlpha(i, n);
                    if (j == i) continue;

                    double alphaIOld = alphas[i];
                    double alphaJOld = alphas[j];
                    double L, H;

                    if (labels.get(i).equals(labels.get(j))) {
                        L = Math.max(0, alphas[i] + alphas[j] - C);
                        H = Math.min(C, alphas[i] + alphas[j]);
                    } else {
                        L = Math.max(0, alphas[j] - alphas[i]);
                        H = Math.min(C, C + alphas[j] - alphas[i]);
                    }
                    if (Math.abs(L - H) < epsilon) continue;

                    double eta = 2 * K[i][j] - K[i][i] - K[j][j];
                    if (eta >= 0) continue;

                    alphas[j] -= labels.get(j) * (errors[i] - errors[j]) / eta;
                    alphas[j] = Math.min(H, Math.max(L, alphas[j]));
                    if (Math.abs(alphas[j] - alphaJOld) < epsilon) continue;

                    alphas[i] += labels.get(i) * labels.get(j) * (alphaJOld - alphas[j]);

                    // 更新偏置b
                    double b1 = b - errors[i] - labels.get(i) * (alphas[i] - alphaIOld) * K[i][i]
                            - labels.get(j) * (alphas[j] - alphaJOld) * K[i][j];
                    double b2 = b - errors[j] - labels.get(i) * (alphas[i] - alphaIOld) * K[i][j]
                            - labels.get(j) * (alphas[j] - alphaJOld) * K[j][j];

                    if (alphas[i] > 0 && alphas[i] < C) b = b1;
                    else if (alphas[j] > 0 && alphas[j] < C) b = b2;
                    else b = (b1 + b2) / 2.0;

                    numChanged++;
                }
            }
            passes = (numChanged == 0) ? passes + 1 : 0;
        }

        // 提取支持向量
        supportVectors = new ArrayList<>();
        supportLabels = new ArrayList<>();
        supportAlphas = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (alphas[i] > epsilon) {
                supportVectors.add(features.get(i));
                supportLabels.add(labels.get(i));
                supportAlphas.add(alphas[i]);
            }
        }
        System.out.println("训练完成,支持向量数: " + supportVectors.size() + "/" + n);
    }

    /**
     * 对单个特征向量进行分类预测
     * @return 决策函数值(>0表示安全格,<0表示雷格)
     */
    public double predict(PatternFeature feature) {
        double sum = 0.0;
        for (int i = 0; i < supportVectors.size(); i++) {
            sum += supportAlphas.get(i) * supportLabels.get(i)
                 * kernel.compute(supportVectors.get(i).getFeatures(), feature.getFeatures());
        }
        return sum + b;
    }

    /**
     * 计算概率估计(使用sigmoid映射)
     * @return [0,1]区间的安全概率
     */
    public double predictProbability(PatternFeature feature) {
        double f = predict(feature);
        return 1.0 / (1.0 + Math.exp(-f));
    }

    private double computeError(List<PatternFeature> features, List<Double> labels,
                                double[][] K, int index) {
        double sum = 0.0;
        for (int i = 0; i < features.size(); i++) {
            sum += alphas[i] * labels.get(i) * K[index][i];
        }
        return sum + b - labels.get(index);
    }

    private int selectSecondAlpha(int i, int n) {
        int j = (int)(Math.random() * (n - 1));
        return j >= i ? j + 1 : j;
    }
}

ProbabilityEngine.java — SVM辅助概率推理引擎

package solver;

import model.*;
import svm.SVMClassifier;
import java.util.*;

/**
 * 结合SVM模式识别与概率推理的扫雷决策引擎
 * 当传统约束传播无法给出确定性结论时,利用SVM分类器提供概率估计
 */
public class ProbabilityEngine {
    private final SVMClassifier classifier;

    public ProbabilityEngine(SVMClassifier classifier) {
        this.classifier = classifier;
    }

    /**
     * 对棋盘上所有未揭示格子进行SVM概率评估
     * @return 按安全概率降序排列的格子列表
     */
    public List<CellProbability> evaluateBoard(Cell[][] board, int remainingMines) {
        List<CellProbability> results = new ArrayList<>();
        int rows = board.length;
        int cols = board[0].length;

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (board[r][c].getState() == CellState.UNREVEALED) {
                    PatternFeature feature = PatternFeature.extract(board, r, c, remainingMines);
                    double prob = classifier.predictProbability(feature);
                    results.add(new CellProbability(r, c, prob));
                }
            }
        }

        // 按安全概率降序排列
        results.sort((a, b) -> Double.compare(b.probability, a.probability));
        return results;
    }

    /**
     * 打印前N个最安全的格子推荐
     */
    public void printTopRecommendations(Cell[][] board, int remainingMines, int topN) {
        List<CellProbability> evals = evaluateBoard(board, remainingMines);
        System.out.println("=== SVM概率推理推荐 ===");
        for (int i = 0; i < Math.min(topN, evals.size()); i++) {
            CellProbability cp = evals.get(i);
            System.out.printf("  排名%d: 位置(%d,%d) 安全概率=%.4f%n",
                i + 1, cp.row, cp.col, cp.probability);
        }
    }

    public static class CellProbability {
        public final int row, col;
        public final double probability;

        public CellProbability(int row, int col, double probability) {
            this.row = row;
            this.col = col;
            this.probability = probability;
        }
    }
}

Main.java — 完整运行示例

import model.*;
import svm.*;
import solver.*;
import java.util.*;

/**
 * 扫雷SVM求解器主程序
 * 演示:训练SVM分类器 -> 生成测试棋盘 -> 概率推理决策
 */
public class Main {
    public static void main(String[] args) {
        // 1. 生成训练数据(模拟大量局部模式)
        System.out.println("=== 生成训练数据 ===");
        List<PatternFeature> trainFeatures = new ArrayList<>();
        List<Double> trainLabels = new ArrayList<>();

        Random rand = new Random(42);
        int trainSize = 2000;
        for (int i = 0; i < trainSize; i++) {
            // 生成随机的局部模式特征(简化模拟)
            double[] feats = new double[PatternFeature.FEATURE_DIM];
            for (int j = 0; j < feats.length; j++) {
                feats[j] = rand.nextDouble() * 2 - 1; // [-1, 1]
            }
            // 模拟标签:邻域数字和>3时更可能是安全格
            double labelScore = feats[0] + feats[1] + feats[2] + feats[3];
            int row = rand.nextInt(16);
            int col = rand.nextInt(16);
            trainFeatures.add(new PatternFeature(feats, row, col));
            trainLabels.add(labelScore > 0.5 ? 1.0 : -1.0);
        }

        // 2. 训练SVM分类器
        System.out.println("=== 训练SVM分类器(RBF核) ===");
        Kernel rbf = new RBFKernel(0.5);
        SVMClassifier svm = new SVMClassifier(rbf, 1.0, 0.001);
        svm.train(trainFeatures, trainLabels);

        // 3. 创建测试棋盘(10x10,10个雷)
        System.out.println("\n=== 创建测试扫雷棋盘 ===");
        int rows = 10, cols = 10, mines = 10;
        Cell[][] board = createBoard(rows, cols, mines, rand);

        // 4. 执行概率推理
        ProbabilityEngine engine = new ProbabilityEngine(svm);
        engine.printTopRecommendations(board, mines, 5);

        System.out.println("\n=== 程序执行完成 ===");
    }

    private static Cell[][] createBoard(int rows, int cols, int mines, Random rand) {
        Cell[][] board = new Cell[rows][cols];
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                board[r][c] = new Cell();
            }
        }

        // 随机放置雷
        int placed = 0;
        while (placed < mines) {
            int r = rand.nextInt(rows);
            int c = rand.nextInt(cols);
            if (!board[r][c].isMine()) {
                board[r][c].setMine(true);
                placed++;
            }
        }

        // 计算邻域雷数
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (board[r][c].isMine()) {
                    board[r][c].setNeighborMines(-1);
                    continue;
                }
                int count = 0;
                for (int dr = -1; dr <= 1; dr++) {
                    for (int dc = -1; dc <= 1; dc++) {
                        int nr = r + dr, nc = c + dc;
                        if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc].isMine()) {
                            count++;
                        }
                    }
                }
                board[r][c].setNeighborMines(count);
            }
        }

        // 揭示部分格子作为初始状态
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (!board[r][c].isMine() && rand.nextDouble() < 0.3) {
                    board[r][c].setState(CellState.REVEALED);
                }
            }
        }
        return board;
    }
}

算法复杂度分析

模块 时间复杂度 空间复杂度 说明
特征提取 O(1) O(1) 每个未揭示格子固定18维特征
SVM训练(SMO) O(n² × passes) O(n²) n为训练样本数,需预计算核矩阵
概率预测 O(s) O(1) s为支持向量数量,通常s << n
全棋盘评估 O(s × u) O(u) u为未揭示格子数

总结

本文创新性地将支持向量机引入扫雷求解框架,通过RBF核函数将复杂的局部约束模式映射到高维空间进行分类。与传统纯概率推理相比,SVM方法能够学习到更复杂的模式特征(如”特定数字排列下某格必然是安全的”),在边界情况和不完全信息区域提供更鲁棒的决策支持。该方法的核心价值在于:将逻辑推理问题转化为模式识别问题,展示了机器学习算法在传统益智游戏中的创新应用。