关于java:如何实现高效的Alpha-Beta剪枝游戏搜索树?

How to implement efficient Alpha-Beta pruning Game Search Tree?

我正在尝试学习人工智能以及如何在程序中实现它。最容易开始的地方可能是简单的游戏(在本例中为井字游戏)和游戏搜索树(递归调用;不是实际的数据结构)。我在有关该主题的讲座中发现了这个非常有用的视频。

我遇到的问题是对算法的第一次调用需要很长时间(大约 15 秒)才能执行。我已经在整个代码中放置了调试日志输出,看起来它调用了算法的某些部分的次数过多。

以下是为计算机选择最佳移动的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    public Best chooseMove(boolean side, int prevScore, int alpha, int beta){
    Best myBest = new Best();
    Best reply;

    if (prevScore == COMPUTER_WIN || prevScore == HUMAN_WIN || prevScore == DRAW){
        myBest.score = prevScore;
        return myBest;
    }

    if (side == COMPUTER){
        myBest.score = alpha;
    }else{
        myBest.score = beta;
    }
    Log.d(TAG,"Alpha:" + alpha +" Beta:" + beta +" prevScore:" + prevScore);
    Move[] moveList = myBest.move.getAllLegalMoves(board);
    for (Move m : moveList){
        String choice;
        if (side == HUMAN){
            choice = playerChoice;
        }else if (side == COMPUTER && playerChoice.equals("X")){
            choice ="O";
        }else{
            choice ="X";
        }
        Log.d(TAG,"Current Move: column-" + m.getColumn() +" row-" + m.getRow());
        int p = makeMove(m, choice, side);
        reply = chooseMove(!side, p, alpha, beta);
        undoMove(m);
        if ((side == COMPUTER) && (reply.score > myBest.score)){
            myBest.move = m;
            myBest.score = reply.score;
            alpha = reply.score;
        }else if((side == HUMAN) && (reply.score < myBest.score)){
            myBest.move = m;
            myBest.score = reply.score;
            beta = reply.score;
        }//end of if-else statement
        if (alpha >= beta) return myBest;
    }//end of for loop
    return myBest;
}

如果位置为空,则 makeMove 方法进行移动并返回一个值(-1 - 人类获胜,0 - 平局,1 - 计算机获胜,-2 或 2 - 否则)。虽然我相信错误可能出在 getAllLegalMoves 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    public Move[] getAllLegalMoves(String[][] grid){
    //I'm unsure whether this method really belongs in this class or in the grid class, though, either way it shouldn't matter.
    items = 0;
    moveList = null;
    Move move = new Move();

    for (int i = 0; i < 3; i++){
        for(int j = 0; j < 3; j++){
            Log.d(TAG,"At Column:" + i +" At Row:" + j);
            if(grid[i][j] == null || grid[i][j].equals("")){
                Log.d(TAG,"Is Empty");
                items++;
                if(moveList == null || moveList.length < items){
                    resize();
                }//end of second if statement
                move.setRow(j);
                move.setColumn(i);
                moveList[items - 1] = move;
            }//end of first if statement
        }//end of second loop
    }//end of first loop
    for (int k = 0; k < moveList.length; k++){
        Log.d(TAG,"Count:" + k +" Column:" + moveList[k].getColumn() +" Row:" + moveList[k].getRow());
    }
    return moveList;
}

private void resize(){
    Move[] b = new Move[items];
    for (int i = 0; i < items - 1; i++){
        b[i] = moveList[i];
    }
    moveList = b;
}

总而言之:是什么导致我的决定,选择最佳行动,需要这么长时间?我错过了什么?有没有更简单的方法来实现这个算法?任何帮助或建议将不胜感激,谢谢!


一个带有 alpha beta 剪枝的极小极大树应该被可视化为一棵树,树的每个节点都是一个可能的移动,很多轮到未来,它的子节点是所有可以从中获取的移动。

为了尽可能快并保证您只需要与您正在向前看的移动数量呈线性关系的空间,您可以进行深度优先搜索并从一侧"扫描"到另一侧。例如,如果您想象正在构建整个树,那么您的程序实际上一次只会构建一个从引导到根的单链,并丢弃它完成的任何部分。

此时我将复制维基百科的伪代码,因为它真的非常简洁明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function alphabeta(node, depth, α, β, Player)        
    if  depth = 0 or node is a terminal node
        return score
    if  Player = MaxPlayer
        for each child of node
            α := max(α, alphabeta(child, depth-1, α, β, not(Player) ))    
            if β ≤ α
                break                             (* Beta cut-off *)
        return α
    else
        for each child of node
            β := min(β, alphabeta(child, depth-1, α, β, not(Player) ))    
            if β ≤ α
                break                             (* Alpha cut-off *)
        return β

注意事项:

-\\'for each child of node\\' - 与其编辑当前棋盘的状态,不如创建一个全新棋盘,它是应用移动的结果。通过使用不可变对象,您的代码将不太容易出现错误,并且总体上可以更快地进行推理。

-要使用此方法,请在当前状态下为您可以进行的所有可能移动调用它,将其深度 -1,-Infinity 用于 alpha,Infinity 用于 beta,并且应该从不动玩家开始\\轮到这些调用中的每一个 - 返回最高值的调用是最好的调用。

它在概念上非常非常简单。如果你编码正确,那么你永远不会一次实例化超过(深度)板,你永远不会考虑无意义的分支等等。


我不会为你分析你的代码,但是因为这是一个很好的编码 kata,所以我为井字游戏写了一个小 ai:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import java.math.BigDecimal;

public class Board {

    /**
     * -1: opponent
     * 0: empty
     * 1: player
     */

    int[][] cells = new int[3][3];

    /**
     * the best move calculated by eval(), or -1 if no more moves are possible
     */

    int bestX, bestY;

    int winner() {
        // row
        for (int y = 0; y < 3; y++) {
            if (cells[0][y] == cells[1][y] && cells[1][y] == cells[2][y]) {
                if (cells[0][y] != 0) {
                    return cells[0][y];
                }
            }
        }

        // column
        for (int x = 0; x < 3; x++) {
            if (cells[x][0] == cells[x][1] && cells[x][1] == cells[x][2]) {
                if (cells[x][0] != 0) {
                    return cells[x][0];
                }
            }
        }

        // 1st diagonal
        if (cells[0][0] == cells[1][1] && cells[1][1] == cells[2][2]) {
            if (cells[0][0] != 0) {
                return cells[0][0];
            }
        }

        // 2nd diagonal
        if (cells[2][0] == cells[1][1] && cells[1][1] == cells[0][2]) {
            if (cells[2][0] != 0) {
                return cells[2][0];
            }
        }

        return 0; // nobody has won
    }

    /**
     * @return 1 if side wins, 0 for a draw, -1 if opponent wins
     */

    int eval(int side) {
        int winner = winner();
        if (winner != 0) {
            return side * winner;
        } else {
            int bestX = -1;
            int bestY = -1;
            int bestValue = Integer.MIN_VALUE;
        loop:
            for (int y = 0; y < 3; y++) {
                for (int x = 0; x < 3; x++) {
                    if (cells[x][y] == 0) {
                        cells[x][y] = side;
                        int value = -eval(-side);
                        cells[x][y] = 0;

                        if (value > bestValue) {
                            bestValue = value;
                            bestX = x;
                            bestY = y;
                            if (bestValue == 1) {
                                // it won't get any better, we might as well stop thinking
                                break loop;
                            }
                        }
                    }
                }
            }
            this.bestX = bestX;
            this.bestY = bestY;
            if (bestValue == Integer.MIN_VALUE) {
                // there were no moves left, it must be a draw!
                return 0;
            } else {
                return bestValue;
            }
        }
    }

    void move(int side) {
        eval(side);
        if (bestX == -1) {
            return;
        }
        cells[bestX][bestY] = side;
        System.out.println(this);

        int w = winner();
        if (w != 0) {
            System.out.println("Game over!");
        } else {
            move(-side);
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        char[] c = {'O', ' ', 'X'};
        for (int y = 0; y < 3; y++) {
            for (int x = 0; x < 3; x++) {
                sb.append(c[cells[x][y] + 1]);
            }
            sb.append('\
'
);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        long start = System.nanoTime();
        Board b = new Board();
        b.move(1);
        long end = System.nanoTime();
        System.out.println(new BigDecimal(end - start).movePointLeft(9));
    }
}

精明的读者会注意到我没有使用 alpha/beta 截止值。尽管如此,在我有点过时的笔记本上,这在 0.015 秒内完成了游戏......

没有分析您的代码,我无法确定问题出在哪里。但是,您在搜索树的每个节点上记录每个可能的移动可能与它有关。