每日算法 — 使用java实现象棋:Alpha-Beta剪枝与置换表优化

引言

中国象棋是流传千年的双人博弈游戏,其状态空间复杂度约为10^48,远超国际象棋和围棋。要实现一个高水平的象棋AI,核心在于高效的搜索算法和精准的局势评估。本文将使用Java实现象棋AI引擎,重点讲解Alpha-Beta剪枝算法、置换表(Transposition Table)缓存优化以及历史启发式(History Heuristic)着法排序策略。

象棋状态表示

public class ChineseChessBoard {
    // 棋盘大小:10行 x 9列
    public static final int ROWS = 10;
    public static final int COLS = 9;

    // 棋子编码(正数为红方,负数为黑方)
    public static final int EMPTY = 0;
    public static final int RED_KING = 1, RED_ADVISOR = 2, RED_ELEPHANT = 3;
    public static final int RED_HORSE = 4, RED_ROOK = 5, RED_CANNON = 6, RED_PAWN = 7;
    public static final int BLACK_KING = -1, BLACK_ADVISOR = -2, BLACK_ELEPHANT = -3;
    public static final int BLACK_HORSE = -4, BLACK_ROOK = -5, BLACK_CANNON = -6, BLACK_PAWN = -7;

    private final int[][] board = new int[ROWS][COLS];

    // Zobrist哈希值(用于置换表)
    private long zobristKey;
    private static final long[][][] ZOBRIST_TABLE = new long[ROWS][COLS][15];

    static {
        Random rand = new Random(0x5F3759DF); // 固定种子保证可复现
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                for (int p = 0; p < 15; p++) {
                    ZOBRIST_TABLE[r][c][p] = rand.nextLong();
                }
            }
        }
    }

    public void setPiece(int row, int col, int piece) {
        int oldPiece = board[row][col];
        if (oldPiece != EMPTY) {
            zobristKey ^= ZOBRIST_TABLE[row][col][oldPiece + 7];
        }
        if (piece != EMPTY) {
            zobristKey ^= ZOBRIST_TABLE[row][col][piece + 7];
        }
        board[row][col] = piece;
    }

    public long getZobristKey() {
        return zobristKey;
    }
}

局势评估函数

public class Evaluation {
    // 基础棋子分值
    private static final int[] PIECE_VALUE = {0, 10000, 250, 250, 400, 900, 450, 100,
                                               0, -10000, -250, -250, -400, -900, -450, -100};

    // 位置加成表(以红方视角,黑方需要镜像)
    private static final int[][] PAWN_POSITION = {
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 10, 10, 10, 0, 0, 0},
        {20, 20, 20, 30, 30, 30, 20, 20, 20},
        {30, 30, 30, 40, 40, 40, 30, 30, 30},
        {40, 40, 40, 50, 50, 50, 40, 40, 40},
        {50, 50, 50, 60, 60, 60, 50, 50, 50}
    };

    /**
     * 评估当前局面分数(红方为正,黑方为负)
     */
    public int evaluate(ChineseChessBoard board) {
        int score = 0;
        for (int r = 0; r < ChineseChessBoard.ROWS; r++) {
            for (int c = 0; c < ChineseChessBoard.COLS; c++) {
                int piece = board.getPiece(r, c);
                if (piece != ChineseChessBoard.EMPTY) {
                    score += PIECE_VALUE[piece + 7];
                    // 位置加成
                    if (Math.abs(piece) == ChineseChessBoard.RED_PAWN) {
                        int pr = piece > 0 ? r : (9 - r);
                        score += (piece > 0 ? 1 : -1) * PAWN_POSITION[pr][c];
                    }
                }
            }
        }
        // 灵活性评估:计算各方有效着法数
        score += mobilityBonus(board, true) - mobilityBonus(board, false);
        return score;
    }

    private int mobilityBonus(ChineseChessBoard board, boolean isRed) {
        // 简化实现:统计可移动位置数量
        return generateAllMoves(board, isRed).size() * 5;
    }

    public List<Move> generateAllMoves(ChineseChessBoard board, boolean isRed) {
        List<Move> moves = new ArrayList<>();
        for (int r = 0; r < ChineseChessBoard.ROWS; r++) {
            for (int c = 0; c < ChineseChessBoard.COLS; c++) {
                int piece = board.getPiece(r, c);
                if ((isRed && piece > 0) || (!isRed && piece < 0)) {
                    moves.addAll(generatePieceMoves(board, r, c, piece));
                }
            }
        }
        return moves;
    }

    private List<Move> generatePieceMoves(ChineseChessBoard board, int r, int c, int piece) {
        List<Move> moves = new ArrayList<>();
        // 根据棋子类型生成合法着法(简化示意)
        int[][] directions = getDirections(piece);
        for (int[] d : directions) {
            int nr = r + d[0], nc = c + d[1];
            if (isValidMove(board, r, c, nr, nc, piece)) {
                moves.add(new Move(r, c, nr, nc));
            }
        }
        return moves;
    }

    private int[][] getDirections(int piece) {
        // 简化方向定义
        return switch (Math.abs(piece)) {
            case 5 -> new int[][]{{-1,0},{1,0},{0,-1},{0,1}}; // 车
            case 4 -> new int[][]{{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}}; // 马
            default -> new int[][]{{-1,0},{1,0},{0,-1},{0,1}};
        };
    }

    private boolean isValidMove(ChineseChessBoard board, int fr, int fc, int tr, int tc, int piece) {
        // 边界检查
        if (tr < 0 || tr >= ChineseChessBoard.ROWS || tc < 0 || tc >= ChineseChessBoard.COLS) return false;
        // 不能吃己方棋子
        int target = board.getPiece(tr, tc);
        if (target != ChineseChessBoard.EMPTY && (piece * target > 0)) return false;
        return true;
    }
}

class Move {
    int fromRow, fromCol, toRow, toCol;
    Move(int fr, int fc, int tr, int tc) {
        this.fromRow = fr; this.fromCol = fc;
        this.toRow = tr; this.toCol = tc;
    }
}

Alpha-Beta剪枝与置换表

public class ChessEngine {
    private static final int MAX_DEPTH = 6;
    private static final int INFINITY = 1000000;

    // 置换表
    private final Map<Long, TranspositionEntry> transpositionTable = new HashMap<>();
    // 历史启发表
    private final int[][][][] historyTable = new int[10][9][10][9];

    private int nodesSearched = 0;
    private int cutoffs = 0;

    static class TranspositionEntry {
        int depth, score, flag; // flag: 0=exact, 1=lower, 2=upper
        Move bestMove;
        TranspositionEntry(int d, int s, int f, Move m) {
            depth = d; score = s; flag = f; bestMove = m;
        }
    }

    /**
     * Alpha-Beta搜索主入口
     */
    public Move searchBestMove(ChineseChessBoard board, boolean isRed) {
        nodesSearched = 0;
        cutoffs = 0;
        transpositionTable.clear();

        Move bestMove = null;
        int bestScore = -INFINITY;

        List<Move> moves = new Evaluation().generateAllMoves(board, isRed);
        sortMoves(moves, board);

        for (Move move : moves) {
            board.makeMove(move);
            int score = -alphaBeta(board, MAX_DEPTH - 1, -INFINITY, -bestScore, !isRed);
            board.undoMove(move);

            if (score > bestScore) {
                bestScore = score;
                bestMove = move;
            }
        }

        System.out.printf("搜索节点: %d, 剪枝次数: %d, 最佳分数: %d%n",
            nodesSearched, cutoffs, bestScore);
        return bestMove;
    }

    /**
     * 带置换表的Alpha-Beta剪枝
     */
    private int alphaBeta(ChineseChessBoard board, int depth, int alpha, int beta, boolean isRed) {
        nodesSearched++;

        // 置换表查找
        long key = board.getZobristKey();
        TranspositionEntry entry = transpositionTable.get(key);
        if (entry != null && entry.depth >= depth) {
            if (entry.flag == 0) return entry.score;
            if (entry.flag == 1 && entry.score >= beta) return entry.score;
            if (entry.flag == 2 && entry.score <= alpha) return entry.score;
        }

        if (depth == 0) {
            return new Evaluation().evaluate(board);
        }

        List<Move> moves = new Evaluation().generateAllMoves(board, isRed);
        if (moves.isEmpty()) {
            return isRed ? -INFINITY + (MAX_DEPTH - depth) : INFINITY - (MAX_DEPTH - depth);
        }

        // 历史启发排序:优先搜索历史得分高的着法
        moves.sort((a, b) -> historyTable[b.fromRow][b.fromCol][b.toRow][b.toCol]
                          - historyTable[a.fromRow][a.fromCol][a.toRow][a.toCol]);

        Move bestMove = null;
        int originalAlpha = alpha;

        for (Move move : moves) {
            board.makeMove(move);
            int score = -alphaBeta(board, depth - 1, -beta, -alpha, !isRed);
            board.undoMove(move);

            if (score >= beta) {
                // Beta剪枝
                cutoffs++;
                historyTable[move.fromRow][move.fromCol][move.toRow][move.toCol] += depth * depth;
                transpositionTable.put(key, new TranspositionEntry(depth, beta, 1, move));
                return beta;
            }

            if (score > alpha) {
                alpha = score;
                bestMove = move;
            }
        }

        // 存入置换表
        int flag = (alpha > originalAlpha) ? 0 : 2;
        transpositionTable.put(key, new TranspositionEntry(depth, alpha, flag, bestMove));
        return alpha;
    }

    private void sortMoves(List<Move> moves, ChineseChessBoard board) {
        // 初始排序:优先考虑吃子着法
        moves.sort((a, b) -> {
            int va = Math.abs(board.getPiece(a.toRow, a.toCol));
            int vb = Math.abs(board.getPiece(b.toRow, b.toCol));
            return vb - va;
        });
    }
}

走法执行与撤销

// 在ChineseChessBoard类中补充
class MoveRecord {
    int fromRow, fromCol, toRow, toCol, fromPiece, toPiece;
}

// makeMove和undoMove方法
public MoveRecord makeMove(Move move) {
    MoveRecord record = new MoveRecord();
    record.fromRow = move.fromRow; record.fromCol = move.fromCol;
    record.toRow = move.toRow; record.toCol = move.toCol;
    record.fromPiece = getPiece(move.fromRow, move.fromCol);
    record.toPiece = getPiece(move.toRow, move.toCol);

    setPiece(move.toRow, move.toCol, record.fromPiece);
    setPiece(move.fromRow, move.fromCol, EMPTY);
    return record;
}

public void undoMove(MoveRecord record) {
    setPiece(record.fromRow, record.fromCol, record.fromPiece);
    setPiece(record.toRow, record.toCol, record.toPiece);
}

复杂度分析

模块 时间复杂度 空间复杂度
着法生成 O(N) per piece O(1)
Alpha-Beta搜索(无剪枝) O(b^d) O(d)
Alpha-Beta搜索(理想剪枝) O(b^(d/2)) O(d)
置换表查询 O(1) amortized O(T)

其中b为平均分支因子(约40-50),d为搜索深度,T为置换表容量。

总结

本文实现了中国象棋AI的核心搜索框架,通过Alpha-Beta剪枝将搜索空间从指数级降为接近平方根级别,置换表避免了重复局面的重复计算,历史启发式则进一步优化了着法排序效率。在实际对局中,配合更深的搜索深度和更精细的评估函数,可以构建出具有相当水平的象棋AI程序。