每日算法 — 使用java实现三维井字棋:三维空间Minimax博弈树与Alpha-Beta剪枝

三维井字棋(3D Tic-Tac-Toe)是经典井字棋的三维扩展,玩家在一个 3×3×3 的立方体空间内轮流落子,率先在任意直线(水平、垂直、纵深或对角线)上连成三子者获胜。相比二维版本,三维棋盘提供了 49 条可能的获胜直线,状态空间复杂度大幅提升,使得简单的穷举难以应对,必须借助博弈树搜索与剪枝算法。

一、游戏规则与状态表示

三维井字棋使用 3×3×3 的立体棋盘,坐标用 (x, y, z) 表示,其中 x, y, z ∈ {0, 1, 2}。获胜条件为在以下任意方向的直线上占据三个连续位置:
– 轴向直线:沿 X、Y、Z 三个坐标轴方向,共 27 条
– 平面对角线:每个 3×3 平面内的对角线,共 18 条
– 体对角线:贯穿立方体的空间对角线,共 4 条

棋盘状态用一个三维整型数组 board[3][3][3] 表示,0 为空,1 为玩家(Max),-1 为 AI(Min)。

二、核心算法:Minimax 与 Alpha-Beta 剪枝

Minimax 算法通过递归模拟双方的最优走法来评估当前局面的价值。在三维井字棋中,由于分支因子约为 20~25,纯 Minimax 在深度较大时效率低下,因此引入 Alpha-Beta 剪枝来剔除不可能影响最终决策的子树。

2.1 局面评估函数

评估函数是 AI 决策质量的关键。我们对每条获胜直线进行评分:
– 若一条直线上有 3 个己方棋子:+10000
– 若一条直线上有 2 个己方棋子且 1 个空位:+100
– 若一条直线上有 1 个己方棋子且 2 个空位:+10
– 若一条直线上有 2 个对方棋子且 1 个空位:-500(必须阻挡)

2.2 Alpha-Beta 剪枝

在递归搜索过程中维护两个值:
– alpha:Max 玩家(玩家)能保证的最小得分下界
– beta:Min 玩家(AI)能保证的最大得分上界

当 alpha >= beta 时,当前分支可以被剪枝,无需继续搜索。

三、Java 实现

import java.util.ArrayList;
import java.util.List;

/**
 * 三维井字棋 - 3D Tic-Tac-Toe
 * 核心算法:Minimax + Alpha-Beta 剪枝
 */
public class TicTacToe3D {
    // 棋盘大小
    private static final int SIZE = 3;
    // 玩家棋子(Max)
    private static final int PLAYER = 1;
    // AI 棋子(Min)
    private static final int AI = -1;
    // 空位
    private static final int EMPTY = 0;
    // 所有可能的获胜直线(坐标三元组列表)
    private static final int[][][] LINES = generateLines();

    private int[][][] board;

    public TicTacToe3D() {
        this.board = new int[SIZE][SIZE][SIZE];
    }

    /**
     * 生成所有49条获胜直线
     * 包括:轴向(27) + 平面对角线(18) + 体对角线(4)
     */
    private static int[][][] generateLines() {
        List<int[][]> lines = new ArrayList<>();

        // 1. 轴向直线(沿x, y, z轴)
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                // 沿x轴
                lines.add(new int[][]{{0,i,j}, {1,i,j}, {2,i,j}});
                // 沿y轴
                lines.add(new int[][]{{i,0,j}, {i,1,j}, {i,2,j}});
                // 沿z轴
                lines.add(new int[][]{{i,j,0}, {i,j,1}, {i,j,2}});
            }
        }

        // 2. 平面对角线(xy, xz, yz平面)
        for (int k = 0; k < SIZE; k++) {
            // xy平面对角线
            lines.add(new int[][]{{0,0,k}, {1,1,k}, {2,2,k}});
            lines.add(new int[][]{{0,2,k}, {1,1,k}, {2,0,k}});
            // xz平面对角线
            lines.add(new int[][]{{0,k,0}, {1,k,1}, {2,k,2}});
            lines.add(new int[][]{{0,k,2}, {1,k,1}, {2,k,0}});
            // yz平面对角线
            lines.add(new int[][]{{k,0,0}, {k,1,1}, {k,2,2}});
            lines.add(new int[][]{{k,0,2}, {k,1,1}, {k,2,0}});
        }

        // 3. 体对角线(贯穿整个立方体)
        lines.add(new int[][]{{0,0,0}, {1,1,1}, {2,2,2}});
        lines.add(new int[][]{{0,0,2}, {1,1,1}, {2,2,0}});
        lines.add(new int[][]{{0,2,0}, {1,1,1}, {2,0,2}});
        lines.add(new int[][]{{0,2,2}, {1,1,1}, {2,0,0}});

        return lines.toArray(new int[lines.size()][][]);
    }

    /**
     * 检查指定玩家是否获胜
     */
    public boolean checkWin(int player) {
        for (int[][] line : LINES) {
            int count = 0;
            for (int[] pos : line) {
                if (board[pos[0]][pos[1]][pos[2]] == player) {
                    count++;
                }
            }
            if (count == SIZE) {
                return true;
            }
        }
        return false;
    }

    /**
     * 检查棋盘是否已满
     */
    public boolean isFull() {
        for (int x = 0; x < SIZE; x++) {
            for (int y = 0; y < SIZE; y++) {
                for (int z = 0; z < SIZE; z++) {
                    if (board[x][y][z] == EMPTY) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * 获取所有空位坐标
     */
    public List<int[]> getEmptyCells() {
        List<int[]> empties = new ArrayList<>();
        for (int x = 0; x < SIZE; x++) {
            for (int y = 0; y < SIZE; y++) {
                for (int z = 0; z < SIZE; z++) {
                    if (board[x][y][z] == EMPTY) {
                        empties.add(new int[]{x, y, z});
                    }
                }
            }
        }
        return empties;
    }

    /**
     * 局面评估函数:对当前棋盘状态打分
     * 正值有利于玩家,负值有利于AI
     */
    public int evaluate() {
        if (checkWin(PLAYER)) return 10000;
        if (checkWin(AI)) return -10000;

        int score = 0;
        for (int[][] line : LINES) {
            int playerCount = 0;
            int aiCount = 0;
            int emptyCount = 0;

            for (int[] pos : line) {
                int cell = board[pos[0]][pos[1]][pos[2]];
                if (cell == PLAYER) playerCount++;
                else if (cell == AI) aiCount++;
                else emptyCount++;
            }

            // 评分逻辑
            if (playerCount > 0 && aiCount > 0) continue; // 双方都有子,无法成线

            if (playerCount == 2 && emptyCount == 1) score += 100;
            else if (playerCount == 1 && emptyCount == 2) score += 10;
            else if (aiCount == 2 && emptyCount == 1) score -= 500; // 必须阻挡
            else if (aiCount == 1 && emptyCount == 2) score -= 10;
        }
        return score;
    }

    /**
     * Minimax + Alpha-Beta 剪枝
     * @param depth 当前搜索深度
     * @param alpha Max玩家的下界
     * @param beta Min玩家的上界
     * @param isMaximizing 当前是否为Max玩家回合
     * @return 当前局面的评估值
     */
    public int minimax(int depth, int alpha, int beta, boolean isMaximizing) {
        int score = evaluate();

        // 终止条件:某方获胜、棋盘已满或达到最大深度
        if (Math.abs(score) == 10000 || isFull() || depth == 0) {
            return score;
        }

        List<int[]> empties = getEmptyCells();

        if (isMaximizing) {
            int maxEval = Integer.MIN_VALUE;
            for (int[] pos : empties) {
                board[pos[0]][pos[1]][pos[2]] = PLAYER;
                int eval = minimax(depth - 1, alpha, beta, false);
                board[pos[0]][pos[1]][pos[2]] = EMPTY;
                maxEval = Math.max(maxEval, eval);
                alpha = Math.max(alpha, eval);
                if (beta <= alpha) break; // Beta剪枝
            }
            return maxEval;
        } else {
            int minEval = Integer.MAX_VALUE;
            for (int[] pos : empties) {
                board[pos[0]][pos[1]][pos[2]] = AI;
                int eval = minimax(depth - 1, alpha, beta, true);
                board[pos[0]][pos[1]][pos[2]] = EMPTY;
                minEval = Math.min(minEval, eval);
                beta = Math.min(beta, eval);
                if (beta <= alpha) break; // Alpha剪枝
            }
            return minEval;
        }
    }

    /**
     * AI 寻找最佳落子位置
     * @param searchDepth 搜索深度(建议 4-6)
     * @return 最佳落子坐标 [x, y, z]
     */
    public int[] findBestMove(int searchDepth) {
        int bestScore = Integer.MAX_VALUE;
        int[] bestMove = null;

        List<int[]> empties = getEmptyCells();

        for (int[] pos : empties) {
            board[pos[0]][pos[1]][pos[2]] = AI;
            int score = minimax(searchDepth - 1, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
            board[pos[0]][pos[1]][pos[2]] = EMPTY;

            if (score < bestScore) {
                bestScore = score;
                bestMove = pos.clone();
            }
        }
        return bestMove;
    }

    /**
     * 在指定位置落子
     */
    public boolean makeMove(int x, int y, int z, int player) {
        if (x < 0 || x >= SIZE || y < 0 || y >= SIZE || z < 0 || z >= SIZE) {
            return false;
        }
        if (board[x][y][z] != EMPTY) {
            return false;
        }
        board[x][y][z] = player;
        return true;
    }

    /**
     * 打印当前棋盘状态(按Z层展开)
     */
    public void printBoard() {
        for (int z = 0; z < SIZE; z++) {
            System.out.println("Layer z=" + z + ":");
            for (int y = 0; y < SIZE; y++) {
                for (int x = 0; x < SIZE; x++) {
                    char c = board[x][y][z] == PLAYER ? 'X' : 
                             board[x][y][z] == AI ? 'O' : '.';
                    System.out.print(c + " ");
                }
                System.out.println();
            }
            System.out.println();
        }
    }

    /**
     * 主程序:简单对局演示
     */
    public static void main(String[] args) {
        TicTacToe3D game = new TicTacToe3D();

        // 玩家先手落子于中心
        game.makeMove(1, 1, 1, PLAYER);
        System.out.println("Player moves to (1,1,1)");
        game.printBoard();

        // AI 寻找最佳应对
        int[] aiMove = game.findBestMove(4);
        game.makeMove(aiMove[0], aiMove[1], aiMove[2], AI);
        System.out.println("AI moves to (" + aiMove[0] + "," + aiMove[1] + "," + aiMove[2] + ")");
        game.printBoard();

        // 检查胜负
        if (game.checkWin(AI)) {
            System.out.println("AI wins!");
        } else if (game.checkWin(PLAYER)) {
            System.out.println("Player wins!");
        } else {
            System.out.println("Game continues...");
        }
    }
}

四、算法复杂度分析

三维井字棋的状态空间上限为 3^27 ≈ 7.6×10^12,但由于游戏通常在十几步内结束,实际搜索空间远小于此。

指标 复杂度 说明
时间复杂度 O(b^d) b 为分支因子(约 20),d 为搜索深度
Alpha-Beta 剪枝后 O(b^(d/2)) 理想排序下,搜索深度等效翻倍
空间复杂度 O(d) 递归栈深度,等于当前搜索深度

在深度为 4~6 的设置下,AI 能在毫秒级完成决策,同时保持较高的对局水平。

五、总结

三维井字棋将经典游戏升维,使得简单的规则枚举不再适用。通过 Minimax 算法模拟双方最优策略,并借助 Alpha-Beta 剪枝将搜索效率提升数个数量级,AI 能够在复杂的立体空间中找到精妙的三子连线。读者通过本文可以掌握三维状态表示、博弈树搜索以及启发式评估函数的设计方法,这些技术同样适用于五子棋、象棋等更复杂的博弈场景。