每日算法 — 使用java实现孔明棋:回溯搜索与对称性剪枝优化

引言

孔明棋(Peg Solitaire)是源自中国的经典单人棋类游戏,玩家在十字形棋盘上通过跳跃相邻棋子来吃掉被跳过的棋子,最终目标是让棋盘上只剩下一枚棋子并位于中心。本文用Java实现孔明棋的求解系统,重点讲解回溯搜索遍历状态空间、对称性剪枝减少重复搜索,以及启发式评估加速找到最优解路径。

核心算法设计

1. 状态压缩表示

孔明棋标准棋盘为十字形,共33个位置。使用64位长整型(long)进行状态压缩:

  • 每位代表一个棋盘位置,1表示有棋子,0表示空位
  • 中心位置初始为空,其余32个位置有棋子
  • 状态转移:从位置A跳过相邻位置B到达空位C,消除B,移动A到C

2. 回溯搜索与状态转移

DFS递归探索所有可能的跳跃:

  • 遍历所有有棋子的位置作为跳跃起点
  • 检查四个方向是否有相邻棋子和目标空位
  • 执行跳跃生成新状态,递归搜索
  • 回溯恢复状态

3. 对称性剪枝

孔明棋棋盘具有旋转对称性和镜像对称性。使用规范化(canonical form)技术:

  • 对每种状态生成其所有对称变体(旋转0°、90°、180°、270°和镜像)
  • 取字典序最小的表示作为该状态的”规范形式”
  • 仅搜索规范形式,遇到非规范形式直接剪枝
  • 可进一步用HashSet记录已访问的规范状态,避免重复搜索

4. 启发式评估

用于评估当前状态距离终局的接近程度:

  • 孤立棋子惩罚:无法被跳跃吃掉的棋子难以消除
  • 中心聚集奖励:棋子越靠近中心,越容易参与后续跳跃
  • 可行动作数量:可行动作越少,越接近终局或死局

完整Java实现

import java.util.*;

/**
 * 孔明棋核心算法实现
 * 包含状态压缩、回溯搜索、对称性剪枝与启发式评估
 */
public class PegSolitaire {

    // 标准英式孔明棋33孔位的坐标表示 (行, 列)
    // 棋盘布局:中心(3,3)为空,有效位置如下
    static final int[][] POSITIONS = {
        {0,2},{0,3},{0,4},
        {1,2},{1,3},{1,4},
        {2,0},{2,1},{2,2},{2,3},{2,4},{2,5},{2,6},
        {3,0},{3,1},{3,2},{3,3},{3,4},{3,5},{3,6},
        {4,0},{4,1},{4,2},{4,3},{4,4},{4,5},{4,6},
        {5,2},{5,3},{5,4},
        {6,2},{6,3},{6,4}
    };

    static final int N = 33; // 总孔位数
    static final int CENTER = 16; // 中心位置索引 (3,3)

    // 坐标到索引的映射
    static final Map<String, Integer> POS_TO_INDEX = new HashMap<>();
    static final int[][] INDEX_TO_POS = new int[N][2];

    // 每个位置在四个方向的邻居关系(跳跃方向)
    // jumpMoves[i][dir] = {中间位置索引, 目标位置索引},-1表示不可跳
    static final int[][][] JUMP_MOVES = new int[N][4][2];

    static {
        for (int i = 0; i < N; i++) {
            POS_TO_INDEX.put(POSITIONS[i][0] + "," + POSITIONS[i][1], i);
            INDEX_TO_POS[i] = POSITIONS[i];
        }

        int[] dr = {-1, 1, 0, 0}; // 上、下、左、右
        int[] dc = {0, 0, -1, 1};

        for (int i = 0; i < N; i++) {
            int r = POSITIONS[i][0], c = POSITIONS[i][1];
            for (int d = 0; d < 4; d++) {
                int midR = r + dr[d], midC = c + dc[d];
                int endR = r + 2 * dr[d], endC = c + 2 * dc[d];

                String midKey = midR + "," + midC;
                String endKey = endR + "," + endC;

                if (POS_TO_INDEX.containsKey(midKey) && POS_TO_INDEX.containsKey(endKey)) {
                    JUMP_MOVES[i][d][0] = POS_TO_INDEX.get(midKey); // 被跳过的中间位置
                    JUMP_MOVES[i][d][1] = POS_TO_INDEX.get(endKey); // 落点位置
                } else {
                    JUMP_MOVES[i][d][0] = -1;
                    JUMP_MOVES[i][d][1] = -1;
                }
            }
        }
    }

    // 方向名称
    static final String[] DIR_NAME = {"上", "下", "左", "右"};

    long initialState;    // 初始状态(只有中心为空)
    long targetState;     // 目标状态(只有中心有棋子)
    int bestMovesCount = Integer.MAX_VALUE;
    List<Move> bestSolution = null;
    Set<Long> visited = new HashSet<>();

    public PegSolitaire() {
        // 初始状态:除中心外所有位置有棋子
        initialState = (1L << N) - 1; // 33位全1
        initialState &= ~(1L << CENTER); // 中心位置清空

        // 目标状态:只有中心有棋子
        targetState = 1L << CENTER;
    }

    /**
     * 检查某位置是否有棋子
     */
    boolean hasPeg(long state, int pos) {
        return (state & (1L << pos)) != 0;
    }

    /**
     * 执行跳跃动作,返回新状态
     */
    long applyMove(long state, int from, int mid, int to) {
        // 清除起点,清除中间被吃的棋子,设置落点
        return (state & ~(1L << from) & ~(1L << mid)) | (1L << to);
    }

    /**
     * 生成所有可能的跳跃动作
     */
    List<Move> generateMoves(long state) {
        List<Move> moves = new ArrayList<>();
        for (int from = 0; from < N; from++) {
            if (!hasPeg(state, from)) continue;
            for (int d = 0; d < 4; d++) {
                int mid = JUMP_MOVES[from][d][0];
                int to = JUMP_MOVES[from][d][1];
                if (mid < 0) continue;
                // 需要:起点有棋,中间有棋,落点为空
                if (hasPeg(state, mid) && !hasPeg(state, to)) {
                    moves.add(new Move(from, mid, to, d));
                }
            }
        }
        return moves;
    }

    static class Move {
        int from, mid, to, dir;
        Move(int f, int m, int t, int d) { from = f; mid = m; to = t; dir = d; }
        public String toString() {
            return String.format("(%d,%d) %s 跳至 (%d,%d),吃掉 (%d,%d)",
                INDEX_TO_POS[from][0], INDEX_TO_POS[from][1], DIR_NAME[dir],
                INDEX_TO_POS[to][0], INDEX_TO_POS[to][1],
                INDEX_TO_POS[mid][0], INDEX_TO_POS[mid][1]);
        }
    }

    /**
     * 对称性变换:生成状态的所有对称变体
     * 英式孔明棋有旋转对称(90°、180°、270°)和镜像对称
     * 为简化,这里实现90°旋转和水平镜像
     */
    List<Long> symmetries(long state) {
        List<Long> result = new ArrayList<>();
        result.add(state);

        // 90°顺时针旋转 (r,c) -> (c, 6-r)
        result.add(rotate(state));
        // 180°旋转 = 90°旋转两次
        result.add(rotate(rotate(state)));
        // 270°旋转
        result.add(rotate(rotate(rotate(state))));
        // 水平镜像 (r,c) -> (r, 6-c)
        result.add(mirror(state));
        // 镜像后再旋转
        long mirrored = mirror(state);
        result.add(rotate(mirrored));
        result.add(rotate(rotate(mirrored)));
        result.add(rotate(rotate(rotate(mirrored))));

        return result;
    }

    /**
     * 90°顺时针旋转棋盘状态
     */
    long rotate(long state) {
        long newState = 0;
        for (int i = 0; i < N; i++) {
            if (!hasPeg(state, i)) continue;
            int r = INDEX_TO_POS[i][0], c = INDEX_TO_POS[i][1];
            int nr = c, nc = 6 - r;
            String key = nr + "," + nc;
            if (POS_TO_INDEX.containsKey(key)) {
                newState |= 1L << POS_TO_INDEX.get(key);
            }
        }
        return newState;
    }

    /**
     * 水平镜像棋盘状态
     */
    long mirror(long state) {
        long newState = 0;
        for (int i = 0; i < N; i++) {
            if (!hasPeg(state, i)) continue;
            int r = INDEX_TO_POS[i][0], c = INDEX_TO_POS[i][1];
            int nr = r, nc = 6 - c;
            String key = nr + "," + nc;
            if (POS_TO_INDEX.containsKey(key)) {
                newState |= 1L << POS_TO_INDEX.get(key);
            }
        }
        return newState;
    }

    /**
     * 获取状态的规范形式(所有对称中最小值)
     * 用于剪枝:等价的对称状态只搜索一次
     */
    long canonical(long state) {
        long min = state;
        for (long sym : symmetries(state)) {
            if (sym < min) min = sym;
        }
        return min;
    }

    /**
     * 启发式评估函数:评估状态距离终局的优劣
     * 值越小越好(剩余棋子数少 + 中心聚集 + 可行动作多)
     */
    int heuristic(long state) {
        int pegCount = Long.bitCount(state);
        int centerBonus = hasPeg(state, CENTER) ? 0 : 5; // 中心有棋更好

        // 孤立棋子惩罚:无法被跳过的棋子
        int isolated = 0;
        for (int i = 0; i < N; i++) {
            if (!hasPeg(state, i)) continue;
            boolean canMove = false;
            for (int d = 0; d < 4; d++) {
                int mid = JUMP_MOVES[i][d][0];
                int to = JUMP_MOVES[i][d][1];
                if (mid >= 0 && hasPeg(state, mid) && !hasPeg(state, to)) {
                    canMove = true; break;
                }
            }
            if (!canMove) isolated += 3;
        }

        return pegCount * 10 + centerBonus + isolated;
    }

    /**
     * 回溯搜索主函数
     * @param state 当前棋盘状态
     * @param moves 已执行的动作序列
     * @param depth 搜索深度(已走步数)
     */
    void dfs(long state, List<Move> moves, int depth) {
        // 找到更优解(剩余棋子更少)
        int pegCount = Long.bitCount(state);
        if (pegCount < bestMovesCount) {
            bestMovesCount = pegCount;
            bestSolution = new ArrayList<>(moves);
            System.out.println("发现更优解:剩余 " + pegCount + " 枚棋子");
        }

        // 剪枝1:到达目标
        if (state == targetState) {
            return;
        }

        // 剪枝2:对称性检查 + 访问集合
        long canon = canonical(state);
        if (visited.contains(canon)) return;
        visited.add(canon);

        // 剪枝3:深度限制(可选,用于限制搜索范围)
        if (depth >= 31) return; // 最多31步(33-2个棋子被吃,每步吃1个)

        // 生成所有合法动作,按启发式排序(更可能到达终局的动作优先)
        List<Move> legalMoves = generateMoves(state);
        legalMoves.sort((a, b) -> {
            long sa = applyMove(state, a.from, a.mid, a.to);
            long sb = applyMove(state, b.from, b.mid, b.to);
            return Integer.compare(heuristic(sa), heuristic(sb));
        });

        for (Move move : legalMoves) {
            long newState = applyMove(state, move.from, move.mid, move.to);
            moves.add(move);
            dfs(newState, moves, depth + 1);
            moves.remove(moves.size() - 1); // 回溯
        }
    }

    /**
     * 求解孔明棋:从初始状态搜索到只剩1枚棋子的最优路径
     */
    public List<Move> solve() {
        visited.clear();
        bestMovesCount = Integer.MAX_VALUE;
        bestSolution = null;
        dfs(initialState, new ArrayList<>(), 0);
        return bestSolution;
    }

    /**
     * 打印棋盘状态
     */
    public void printBoard(long state) {
        char[][] display = new char[7][7];
        for (char[] row : display) Arrays.fill(row, ' ');
        for (int i = 0; i < N; i++) {
            int r = INDEX_TO_POS[i][0], c = INDEX_TO_POS[i][1];
            display[r][c] = hasPeg(state, i) ? '●' : '○';
        }
        for (int r = 0; r < 7; r++) {
            for (int c = 0; c < 7; c++) {
                System.out.print(display[r][c] + " ");
            }
            System.out.println();
        }
    }

    // 演示入口
    public static void main(String[] args) {
        PegSolitaire game = new PegSolitaire();

        System.out.println("=== 孔明棋 初始布局 ===");
        game.printBoard(game.initialState);

        System.out.println("\n开始回溯搜索...");
        long start = System.currentTimeMillis();
        List<Move> solution = game.solve();
        long elapsed = System.currentTimeMillis() - start;

        System.out.println("\n搜索耗时: " + elapsed + "ms");
        System.out.println("最优解剩余棋子: " + game.bestMovesCount);

        if (solution != null) {
            System.out.println("\n最优解路径(前10步):");
            for (int i = 0; i < Math.min(10, solution.size()); i++) {
                System.out.println((i + 1) + ". " + solution.get(i));
            }
        }
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
状态转移 O(1) O(1) 位运算实现
生成合法动作 O(n) O(n) n为33个位置
对称性计算 O(1) O(1) 固定8种对称变体
规范形式 O(1) O(1) 8个值取最小
回溯搜索 O(b^d / s) O(d) b为分支因子,d为深度,s为对称剪枝系数

算法亮点总结

  • 64位状态压缩:用单个long表示整个棋盘,状态转移仅需3次位运算,内存占用极低。
  • 对称性剪枝:8种对称变体取规范形式,等效状态空间缩减约8倍,配合访问集合避免大量重复搜索。
  • 启发式排序:每次按评估函数排序候选动作,优先探索更可能接近终局的分支,显著加速找到优质解。
  • 增量更新最优解:搜索过程中持续记录当前最佳结果,即使未找到完美解也能输出局部最优路径。

这套算法可进一步扩展为支持不同棋盘变体(欧式37孔、三角形)、逆向求解(从终局反推初始布局)、以及基于A*搜索的带权最优路径求解等高级功能。