每日算法 — 使用java实现MCTS:蒙特卡洛树搜索与UCT决策算法

算法概述

蒙特卡洛树搜索(Monte Carlo Tree Search, MCTS)是一种结合了随机模拟与树搜索的决策算法。它通过大量随机对局来评估棋局状态,无需复杂的启发式评估函数即可在复杂博弈中找到近似最优解。MCTS的四个核心阶段为:选择(Selection)、扩展(Expansion)、模拟(Simulation)和反向传播(Backpropagation)。

UCT公式

UCT(Upper Confidence Bound applied to Trees)公式用于平衡探索与利用:

UCT = wins/visits + C * sqrt(ln(parentVisits) / visits)

其中C为探索常数,通常取sqrt(2)。

完整Java实现

import java.util.*;

/**
 * MCTS(蒙特卡洛树搜索)实现 - 以井字棋为例
 */
public class MCTSTicTacToe {

    // 探索常数,平衡探索与利用
    private static final double EXPLORATION_CONSTANT = Math.sqrt(2);
    // 每次决策的模拟次数
    private static final int SIMULATIONS = 5000;

    // 玩家标识
    private static final int PLAYER_X = 1;
    private static final int PLAYER_O = 2;
    private static final int EMPTY = 0;

    /**
     * 树节点:表示博弈树中的一个状态
     */
    static class TreeNode {
        int[][] board;          // 当前棋盘状态
        int player;             // 当前轮到哪个玩家
        TreeNode parent;        // 父节点
        List<TreeNode> children;// 子节点列表
        int visits = 0;         // 访问次数
        double wins = 0;        // 胜利次数(从当前玩家视角)
        boolean isTerminal = false; // 是否为终局
        int terminalResult = 0; // 终局结果:1=X胜, 2=O胜, -1=平局

        TreeNode(int[][] board, int player, TreeNode parent) {
            this.board = copyBoard(board);
            this.player = player;
            this.parent = parent;
            this.children = new ArrayList<>();
            checkTerminal();
        }

        int[][] copyBoard(int[][] src) {
            int[][] dst = new int[3][3];
            for (int i = 0; i < 3; i++)
                dst[i] = src[i].clone();
            return dst;
        }

        void checkTerminal() {
            int winner = checkWinner(board);
            if (winner != 0) {
                isTerminal = true;
                terminalResult = winner;
                return;
            }
            if (isBoardFull(board)) {
                isTerminal = true;
                terminalResult = -1;
            }
        }

        boolean isBoardFull(int[][] b) {
            for (int[] row : b)
                for (int cell : row)
                    if (cell == EMPTY) return false;
            return true;
        }

        int checkWinner(int[][] b) {
            for (int i = 0; i < 3; i++) {
                if (b[i][0] != EMPTY && b[i][0] == b[i][1] && b[i][1] == b[i][2])
                    return b[i][0];
                if (b[0][i] != EMPTY && b[0][i] == b[1][i] && b[1][i] == b[2][i])
                    return b[0][i];
            }
            if (b[0][0] != EMPTY && b[0][0] == b[1][1] && b[1][1] == b[2][2])
                return b[0][0];
            if (b[0][2] != EMPTY && b[0][2] == b[1][1] && b[1][1] == b[2][0])
                return b[0][2];
            return 0;
        }

        List<int[]> getLegalMoves() {
            List<int[]> moves = new ArrayList<>();
            for (int i = 0; i < 3; i++)
                for (int j = 0; j < 3; j++)
                    if (board[i][j] == EMPTY)
                        moves.add(new int[]{i, j});
            return moves;
        }

        boolean isFullyExpanded() {
            return children.size() == getLegalMoves().size();
        }

        TreeNode selectBestChild() {
            TreeNode best = null;
            double bestValue = Double.NEGATIVE_INFINITY;
            for (TreeNode child : children) {
                double uctValue;
                if (child.visits == 0) {
                    uctValue = Double.POSITIVE_INFINITY;
                } else {
                    double winRate = child.wins / child.visits;
                    double exploration = EXPLORATION_CONSTANT *
                            Math.sqrt(Math.log(this.visits) / child.visits);
                    uctValue = winRate + exploration;
                }
                if (uctValue > bestValue) {
                    bestValue = uctValue;
                    best = child;
                }
            }
            return best;
        }
    }

    public int[] findBestMove(int[][] board, int currentPlayer) {
        TreeNode root = new TreeNode(board, currentPlayer, null);
        for (int i = 0; i < SIMULATIONS; i++) {
            TreeNode selected = select(root);
            TreeNode expanded = expand(selected);
            int result = simulate(expanded);
            backpropagate(expanded, result, currentPlayer);
        }
        TreeNode bestChild = null;
        int maxVisits = -1;
        for (TreeNode child : root.children) {
            if (child.visits > maxVisits) {
                maxVisits = child.visits;
                bestChild = child;
            }
        }
        return findMove(root.board, bestChild.board);
    }

    TreeNode select(TreeNode node) {
        while (!node.isTerminal && node.isFullyExpanded()) {
            node = node.selectBestChild();
        }
        return node;
    }

    TreeNode expand(TreeNode node) {
        if (node.isTerminal) return node;
        List<int[]> moves = node.getLegalMoves();
        Set<String> created = new HashSet<>();
        for (TreeNode child : node.children) {
            created.add(boardToKey(child.board));
        }
        for (int[] move : moves) {
            int[][] newBoard = node.copyBoard(node.board);
            newBoard[move[0]][move[1]] = node.player;
            int nextPlayer = (node.player == PLAYER_X) ? PLAYER_O : PLAYER_X;
            if (!created.contains(boardToKey(newBoard))) {
                TreeNode child = new TreeNode(newBoard, nextPlayer, node);
                node.children.add(child);
                return child;
            }
        }
        return node;
    }

    int simulate(TreeNode node) {
        int[][] simBoard = node.copyBoard(node.board);
        int currentPlayer = node.player;
        TreeNode temp = new TreeNode(simBoard, currentPlayer, null);
        while (true) {
            int winner = temp.checkWinner(simBoard);
            if (winner != 0) return winner;
            if (temp.isBoardFull(simBoard)) return -1;
            List<int[]> moves = new ArrayList<>();
            for (int i = 0; i < 3; i++)
                for (int j = 0; j < 3; j++)
                    if (simBoard[i][j] == EMPTY)
                        moves.add(new int[]{i, j});
            int[] move = moves.get((int)(Math.random() * moves.size()));
            simBoard[move[0]][move[1]] = currentPlayer;
            currentPlayer = (currentPlayer == PLAYER_X) ? PLAYER_O : PLAYER_X;
        }
    }

    void backpropagate(TreeNode node, int result, int rootPlayer) {
        TreeNode current = node;
        while (current != null) {
            current.visits++;
            if (result == -1) {
                current.wins += 0.5;
            } else if (result == rootPlayer) {
                current.wins += 1.0;
            }
            current = current.parent;
        }
    }

    String boardToKey(int[][] board) {
        StringBuilder sb = new StringBuilder();
        for (int[] row : board)
            for (int cell : row)
                sb.append(cell);
        return sb.toString();
    }

    int[] findMove(int[][] before, int[][] after) {
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                if (before[i][j] != after[i][j])
                    return new int[]{i, j};
        return null;
    }

    void printBoard(int[][] board) {
        for (int[] row : board) {
            for (int cell : row) {
                char c = (cell == EMPTY) ? '.' : (cell == PLAYER_X ? 'X' : 'O');
                System.out.print(c + " ");
            }
            System.out.println();
        }
        System.out.println();
    }

    public static void main(String[] args) {
        MCTSTicTacToe mcts = new MCTSTicTacToe();
        int[][] board = new int[3][3];
        int currentPlayer = PLAYER_X;
        System.out.println("MCTS井字棋对战演示:");
        mcts.printBoard(board);
        for (int turn = 0; turn < 9; turn++) {
            int[] move = mcts.findBestMove(board, currentPlayer);
            System.out.println((currentPlayer == PLAYER_X ? "X" : "O") + " 选择: (" + move[0] + "," + move[1] + ")");
            board[move[0]][move[1]] = currentPlayer;
            mcts.printBoard(board);
            TreeNode temp = new TreeNode(board, currentPlayer, null);
            int winner = temp.checkWinner(board);
            if (winner != 0) {
                System.out.println((winner == PLAYER_X ? "X" : "O") + " 获胜!");
                return;
            }
            currentPlayer = (currentPlayer == PLAYER_X) ? PLAYER_O : PLAYER_X;
        }
        System.out.println("平局!");
    }
}

算法复杂度分析

  • 时间复杂度:每次模拟需要O(d)时间,其中d为博弈深度。总时间复杂度为O(N * d),N为模拟次数。
  • 空间复杂度:O(N * d)用于存储树节点,每个节点存储棋盘状态和统计信息。

应用场景

MCTS广泛应用于围棋AI(如AlphaGo早期版本)、即时策略游戏、卡牌游戏以及任何具有大状态空间的序贯决策问题。其优势在于无需领域知识即可实现强大的AI决策能力。