极小极大的定义
Minimax算法 又名极小化极大算法,是一种找出失败的最大可能性中的最小值的算法(即最小化对手的最大得益)。通常以递归形式来实现。

Minimax算法常用于棋类等由两方较量的游戏和程序。该算法是一个零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,另一方则选择令对手优势最小化的一个,其输赢的总和为0(有点像能量守恒,就像本身两个玩家都有1点,最后输家要将他的1点给赢家,但整体上还是总共有2点)。很多棋类游戏可以采取此算法,例如tic-tac-toe。

关于极小极大,更多的信息可参考以下文章:


Minimax(wikipedia)


Understanding The MiniMax Algorithm


Minimax Explained


最小最大原理与搜索方法



以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. }