极小极大的定义
Minimax算法 又名极小化极大算法,是一种找出失败的最大可能性中的最小值的算法(即最小化对手的最大得益)。通常以递归形式来实现。
Minimax算法常用于棋类等由两方较量的游戏和程序。该算法是一个零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,另一方则选择令对手优势最小化的一个,其输赢的总和为0(有点像能量守恒,就像本身两个玩家都有1点,最后输家要将他的1点给赢家,但整体上还是总共有2点)。很多棋类游戏可以采取此算法,例如tic-tac-toe。
关于极小极大,更多的信息可参考以下文章:
Understanding The MiniMax Algorithm
以TIC-TAC-TOE为例
tic-tac-toe就是我们小时候玩的井字棋(我这边习惯叫“井字过三关”),不熟悉的可以 参考wiki 。
一般X的玩家先下。设定X玩家的最大利益为正无穷(+∞),O玩家的最大利益为负无穷(-∞),这样我们称X玩家为MAX(因为他总是追求更大的值),成O玩家为MIN(她总是追求更小的值),各自都为争取自己的最大获益而努力。
现在,让我们站在MAX的立场来分析局势(这是必须的,应为你总不能两边倒吧,你喜欢的话也可以选择MIN)。由于MAX是先下的(习惯上X的玩家先下),于是构建出来的博弈树如下(前面两层):
MAX总是会选择MIN最大获利中的最小值(对MAX最有利),同样MIN也会一样,选择对自己最有利的(即MAX有可能获得的最大值)。有点难理解,其实就是自己得不到也不给你得到这样的意思啦,抢先把对对手有利的位置抢占了。你会看出,这是不断往下深钻的,直到最底层(即叶节点)你才能网上回溯,确定那个是对你最有利的。
具体过程会像是这么一个样子的:
但实际情况下,完全遍历一颗博弈树是不现实的,因为层级的节点数是指数级递增的:
层数 节点数
0 1
1 9
2 72
3 504
4 3024
5 15120
6 60480
7 181440
8 362880
完全遍历会很耗时...一般情况下需要限制深钻的层数,在达到限定的层数时就返回一个估算值(通过一个启发式的函数对当前博弈位置进行估值),这样获得的值就不是精确的了(遍历的层数越深越精确,当然和估算函数也有一定关系),但该值依然是足够帮助我们做出决策的。于是,对耗时和精确度需要做一个权衡。一般我们限定其遍历的深度为6(目前多数的象棋游戏也是这么设定的)。
于是,我们站在MAX的角度,评估函数会是这样子的:
Java代码
1. static final int INFINITY = 100 ; // 表示无穷的值
2. static final int WIN = +INFINITY ; // MAX的最大利益为正无穷
3. static final int LOSE = -INFINITY ; // MAX的最小得益(即MIN的最大得益)为负无穷
4. static final int DOUBLE_LINK = INFINITY / 2 ; // 如果同一行、列或对角上连续有两个,赛点
5. static final int INPROGRESS = 1 ; // 仍可继续下(没有胜出或和局)
6. static final int DRAW = 0 ; // 和局
7. static final int
8. 0, 1, 2
9. 3, 4, 5
10. 6, 7, 8
11. 0, 3, 6
12. 1, 4, 7
13. 2, 5, 8
14. 0, 4, 8
15. 2, 4, 6
16. };
17. /**
18. * 估值函数,提供一个启发式的值,决定了游戏AI的高低
19. */
20. public int gameState( char
21. int
22. boolean isFull = true
23.
24. // is game over?
25. for ( int pos = 0; pos < 9; pos++) {
26. char
27. if
28. false
29. break
30. }
31. }
32. // is Max win/lose?
33. for ( int
34. char chess = board[status[0]];
35. if
36. break
37. }
38. int i = 1;
39. for
40. if
41. break
42. }
43. }
44. if
45. result = chess == x ? WIN : LOSE;
46. break
47. }
48. }
49. if
50. if
51. // is draw
52. result = DRAW;
53. else
54. // check double link
55. // finds[0]->'x', finds[1]->'o'
56. int [] finds = new int [2];
57. for ( int
58. char
59. boolean hasEmpty = false
60. int count = 0;
61. for ( int i = 0; i < status.length; i++) {
62. if
63. true
64. else
65. if
66. chess = board[status[i]];
67. }
68. if
69. count++;
70. }
71. }
72. }
73. if (hasEmpty && count > 1) {
74. if
75. 0]++;
76. else
77. 1]++;
78. }
79. }
80. }
81. // check if two in one line
82. if (finds[1] > 0) {
83. result = -DOUBLE_LINK;
84. else if (finds[0] > 0) {
85. result = DOUBLE_LINK;
86. }
87. }
88. }
89. return
90. }
static final int INFINITY = 100 ; // 表示无穷的值
static final int WIN = +INFINITY ; // MAX的最大利益为正无穷
static final int LOSE = -INFINITY ; // MAX的最小得益(即MIN的最大得益)为负无穷
static final int DOUBLE_LINK = INFINITY / 2 ; // 如果同一行、列或对角上连续有两个,赛点
static final int INPROGRESS = 1 ; // 仍可继续下(没有胜出或和局)
static final int DRAW = 0 ; // 和局
static final int [][] WIN_STATUS = {
{ 0, 1, 2 },
{ 3, 4, 5 },
{ 6, 7, 8 },
{ 0, 3, 6 },
{ 1, 4, 7 },
{ 2, 5, 8 },
{ 0, 4, 8 },
{ 2, 4, 6 }
};
/**
* 估值函数,提供一个启发式的值,决定了游戏AI的高低
*/
public int gameState( char [] board ) {
int result = INPROGRESS;
boolean isFull = true ;
// is game over?
for ( int pos = 0; pos < 9; pos++) {
char chess = board[pos];
if ( empty == chess) {
isFull = false ;
break ;
}
}
// is Max win/lose?
for ( int [] status : WIN_STATUS) {
char chess = board[status[0]];
if (chess == empty ) {
break ;
}
int i = 1;
for (; i < status.length; i++) {
if (board[status[i]] != chess) {
break ;
}
}
if (i == status.length) {
result = chess == x ? WIN : LOSE;
break ;
}
}
if (result != WIN & result != LOSE) {
if (isFull) {
// is draw
result = DRAW;
} else {
// check double link
// finds[0]->'x', finds[1]->'o'
int [] finds = new int [2];
for ( int [] status : WIN_STATUS) {
char chess = empty ;
boolean hasEmpty = false ;
int count = 0;
for ( int i = 0; i < status.length; i++) {
if (board[status[i]] == empty ) {
hasEmpty = true ;
} else {
if (chess == empty ) {
chess = board[status[i]];
}
if (board[status[i]] == chess) {
count++;
}
}
}
if (hasEmpty && count > 1) {
if (chess == x ) {
finds[0]++;
} else {
finds[1]++;
}
}
}
// check if two in one line
if (finds[1] > 0) {
result = -DOUBLE_LINK;
} else if (finds[0] > 0) {
result = DOUBLE_LINK;
}
}
}
return result;
}
基于这些,一个限定层数的实现是这样的:
Java代码
1. /**
2. * 以'x'的角度来考虑的极小极大算法
3. */
4. public int minimax( char [] board, int
5. int [] bestMoves = new int [9];
6. int index = 0;
7.
8. int
9. for ( int pos=0; pos<9; pos++){
10.
11. if
12. board[pos] = x ;
13.
14. int
15. if
16. bestValue = value;
17. 0;
18. bestMoves[index] = pos;
19. else
20. if
21. index++;
22. bestMoves[index] = pos;
23. }
24.
25. board[pos] = empty ;
26. }
27.
28. }
29.
30. if (index>1){
31. new Random (System. currentTimeMillis ()).nextInt()>>>1)%index;
32. }
33. return
34.
35. }
36. /**
37. * 对于'x',估值越大对其越有利
38. */
39. public int max( char [] board, int
40.
41. int
42.
43. boolean
44. if (depth==0
45. return
46. }
47.
48. int
49. for ( int pos=0; pos<9; pos++){
50.
51. if
52. // try
53. board[pos] = x ;
54.
55. // maximixing
56. 1));
57.
58. // reset
59. board[pos] = empty ;
60. }
61.
62. }
63.
64. return
65.
66. }
67. /**
68. * 对于'o',估值越小对其越有利
69. */
70. public int min( char [] board, int
71.
72. int
73.
74. boolean
75. if (depth==0
76. return
77. }
78.
79. int
80. for ( int pos=0; pos<9; pos++){
81.
82. if
83. // try
84. board[pos] = o ;
85.
86. // minimixing
87. 1));
88.
89. // reset
90. board[pos] = empty ;
91. }
92.
93. }
94.
95. return
96.
97. }
/**
* 以'x'的角度来考虑的极小极大算法
*/
public int minimax( char [] board, int depth){
int [] bestMoves = new int [9];
int index = 0;
int bestValue = - INFINITY ;
for ( int pos=0; pos<9; pos++){
if (board[pos]== empty ){
board[pos] = x ;
int value = min(board, depth);
if (value>bestValue){
bestValue = value;
index = 0;
bestMoves[index] = pos;
} else
if (value==bestValue){
index++;
bestMoves[index] = pos;
}
board[pos] = empty ;
}
}
if (index>1){
index = ( new Random (System. currentTimeMillis ()).nextInt()>>>1)%index;
}
return bestMoves[index];
}
/**
* 对于'x',估值越大对其越有利
*/
public int max( char [] board, int depth){
int evalValue = gameState (board);
boolean isGameOver = (evalValue== WIN || evalValue== LOSE || evalValue== DRAW );
if (depth==0 || isGameOver){
return evalValue;
}
int bestValue = - INFINITY ;
for ( int pos=0; pos<9; pos++){
if (board[pos]== empty ){
// try
board[pos] = x ;
// maximixing
bestValue = Math. max (bestValue, min(board, depth-1));
// reset
board[pos] = empty ;
}
}
return evalValue;
}
/**
* 对于'o',估值越小对其越有利
*/
public int min( char [] board, int depth){
int evalValue = gameState (board);
boolean isGameOver = (evalValue== WIN || evalValue== LOSE || evalValue== DRAW );
if (depth==0 || isGameOver){
return evalValue;
}
int bestValue = + INFINITY ;
for ( int pos=0; pos<9; pos++){
if (board[pos]== empty ){
// try
board[pos] = o ;
// minimixing
bestValue = Math.min(bestValue, max(board, depth-1));
// reset
board[pos] = empty ;
}
}
return evalValue;
}
Alpha-beta剪枝
另外,通过结合Alpha-beta剪枝能进一步优化效率。Alpha-beta剪枝顾名思义就是裁剪掉一些不必要的分支,以减少遍历的节点数。实际上是通过传递两个参数alpha和beta到递归的极小极大函数中,alpha表示了MAX的最坏情况,beta表示了MIN的最坏情况,因此他们的初始值为负无穷和正无穷。在递归的过程中,在轮到MAX的回合,如果极小极大的值比alpha大,则更新alpha;在MIN的回合中,如果极小极大值比beta小,则更新beta。当alpha和beta相交时(即alpha>=beta),这时该节点的所有子节点对于MAX和MIN双方都不会带来好的获益,所以可以忽略掉(裁剪掉)以该节点为父节点的整棵子树。
根据这一定义,可以很轻易地在上面程序的基础上进行改进:
Java代码
1. /**
2. * 以'x'的角度来考虑的极小极大算法
3. */
4. public int minimax( char [] board, int
5. int [] bestMoves = new int [9];
6. int index = 0;
7.
8. int
9. for ( int pos=0; pos<9; pos++){
10.
11. if
12. board[pos] = x ;
13.
14. int
15. if
16. bestValue = value;
17. 0;
18. bestMoves[index] = pos;
19. else
20. if
21. index++;
22. bestMoves[index] = pos;
23. }
24.
25. board[pos] = empty ;
26. }
27.
28. }
29.
30. if (index>1){
31. new Random (System. currentTimeMillis ()).nextInt()>>>1)%index;
32. }
33. return
34.
35. }
36. /**
37. * 对于'x',估值越大对其越有利
38. */
39. public int max( char [] board, int depth, int alpha, int
40.
41. int
42.
43. boolean
44. if
45. return
46. }
47. if (depth==0
48. return
49. }
50.
51. int
52. for ( int pos=0; pos<9; pos++){
53.
54. if
55. // try
56. board[pos] = x ;
57.
58. // maximixing
59. 1, Math. max (bestValue, alpha), beta));
60.
61. // reset
62. board[pos] = empty ;
63. }
64.
65. }
66.
67. return
68.
69. }
70. /**
71. * 对于'o',估值越小对其越有利
72. */
73. public int min( char [] board, int depth, int alpha, int
74.
75. int
76.
77. boolean
78. if
79. return
80. }
81. // try
82. if (depth==0
83. return
84. }
85.
86. int
87. for ( int pos=0; pos<9; pos++){
88.
89. if
90. // try
91. board[pos] = o ;
92.
93. // minimixing
94. 1, alpha, Math.min(bestValue, beta)));
95.
96. // reset
97. board[pos] = empty ;
98. }
99.
100. }
101.
102. return
103.
104. }
/**
* 以'x'的角度来考虑的极小极大算法
*/
public int minimax( char [] board, int depth){
int [] bestMoves = new int [9];
int index = 0;
int bestValue = - INFINITY ;
for ( int pos=0; pos<9; pos++){
if (board[pos]== empty ){
board[pos] = x ;
int value = min(board, depth, - INFINITY , + INFINITY );
if (value>bestValue){
bestValue = value;
index = 0;
bestMoves[index] = pos;
} else
if (value==bestValue){
index++;
bestMoves[index] = pos;
}
board[pos] = empty ;
}
}
if (index>1){
index = ( new Random (System. currentTimeMillis ()).nextInt()>>>1)%index;
}
return bestMoves[index];
}
/**
* 对于'x',估值越大对其越有利
*/
public int max( char [] board, int depth, int alpha, int beta){
int evalValue = gameState (board);
boolean isGameOver = (evalValue== WIN || evalValue== LOSE || evalValue== DRAW );
if (beta<=alpha){
return evalValue;
}
if (depth==0 || isGameOver){
return evalValue;
}
int bestValue = - INFINITY ;
for ( int pos=0; pos<9; pos++){
if (board[pos]== empty ){
// try
board[pos] = x ;
// maximixing
bestValue = Math. max (bestValue, min(board, depth-1, Math. max (bestValue, alpha), beta));
// reset
board[pos] = empty ;
}
}
return evalValue;
}
/**
* 对于'o',估值越小对其越有利
*/
public int min( char [] board, int depth, int alpha, int beta){
int evalValue = gameState (board);
boolean isGameOver = (evalValue== WIN || evalValue== LOSE || evalValue== DRAW );
if (alpha>=beta){
return evalValue;
}
// try
if (depth==0 || isGameOver || alpha>=beta){
return evalValue;
}
int bestValue = + INFINITY ;
for ( int pos=0; pos<9; pos++){
if (board[pos]== empty ){
// try
board[pos] = o ;
// minimixing
bestValue = Math.min(bestValue, max(board, depth-1, alpha, Math.min(bestValue, beta)));
// reset
board[pos] = empty ;
}
}
return evalValue;
}
*这里对极小极大算法的实现只是其中一种可行性,实际上可能会看到很多种不同的实现方式,但道理是一样的。
使用开局库
同时,你会发现,这样做视乎还不够,特别在一开局。我们都知道,中心的位置是最好的,但是按照我们上面的算法,第一步确实随机的...这在深度受限制的情况下就更显得重要了。于是就引申出了开局库的概念,这是我在某个讲象棋AI的网上看到的,就是给初始的棋盘设定一些格局。
针对上面的例子,我们只要判断是否第一步棋,如果是则想办法让他选择中心的位置(4)。
在WIKI百科中找到一幅图也能作为TIC-TAC-TOE开局库的参考:
于是,估算函数会变成这样的(当然也可以在别的地方修改,只要合理):
Java代码
1. //开局时,每个位置的估值
2. static final int
3. 3, 2, 3,
4. 2, 4, 2,
5. 3, 2, 3
6. };
7. /**
8. * 估值函数,提供一个启发式的值,决定了游戏AI的高低
9. */
10. public int gameState ( char
11. int
12. boolean isFull = true
13. int sum = 0;
14. int index = 0;
15. // is game over?
16. for ( int pos=0; pos<9; pos++){
17. char
18. if
19. false
20. else
21. sum += chess;
22. index = pos;
23. }
24. }
25.
26. // 如果是初始状态,则使用开局库
27. boolean
28. if
29. return (sum== x ?1:-1)*INITIAL_POS_VALUE[index];
30. }
31.
32. // is Max win/lose?
33. for ( int
34. char chess = board[status[0]];
35. if
36. break
37. }
38. int i = 1;
39. for
40. if
41. break
42. }
43. }
44. if
45. result = chess== x ? WIN : LOSE ;
46. break
47. }
48. }
49.
50. if
51.
52. if
53. // is draw
54. result = DRAW ;
55. else
56. // check double link
57. // finds[0]->'x', finds[1]->'o'
58. int [] finds = new int [2];
59. for ( int
60. char
61. boolean hasEmpty = false
62. int count = 0;
63. for ( int i=0; i<status.length; i++){
64. if
65. true
66. else
67. if
68. chess = board[status[i]];
69. }
70. if
71. count++;
72. }
73. }
74. }
75. if (hasEmpty && count>1){
76. if
77. 0]++;
78. else
79. 1]++;
80. }
81. }
82. }
83.
84. // check if two in one line
85. if (finds[1]>0){
86. result = - DOUBLE_LINK ;
87. else
88. if (finds[0]>0){
89. result = DOUBLE_LINK ;
90. }
91.
92. }
93.
94. }
95.
96. return
97.
98. }