极小-极大和负值最大搜索算法的个人理解

最近过年放假,想来时间比较充裕,于是研究了一下棋类游戏制作的原理。我们所熟悉的棋类游戏有五子棋、跳棋、象棋、围棋等,我们在制作这些棋类游戏时,如果是两个玩家对弈,则游戏代码很容易写出来,只要按照下棋规则设计程序即可。但如果是玩家与电脑对弈,则难度就很大了,因为电脑不可能像玩家一样那么智能,如果电脑真能和人一样聪明的话,那么电影<<终结者>>中的画面在我们的现实社会中就要重现了。

任何棋类游戏都要定义一棵有根的树,即博弈树,一个节点就代表下棋的一个局面,子节点就是这个局面再走一步可以到达的下一个局面。博弈树的一层就表示下棋一方的所有着子的棋局。如下图是一个井子棋游戏,偶数层代表了"×"下子的所有可能局面,奇数层代表了"Ο"下子的所有可能局面。对弈双方轮流着子,某一方下子将会使的博弈树增加一层,直到某一方赢棋或是和棋(叶子节点),棋局结束时,博弈树不再增加。

在下棋时,对弈双方的目的就是将死对方,或者避免被将死,如果甲棋手在下子时能找到一步无论乙棋手怎么下子都会输的棋局,那么甲棋手一定会走这步堪称完美的棋法。下子的一方总是会这样考虑下棋:"如果我将子下在这里,对方会将子下在哪里使得他自已最有利,然后我又应该怎么下子使我最有利。。。",下子的一方会这样考虑许多不同的下子位置,然后选择一个对自已最有利,而使对方最不利的下子棋局。

如果棋手在考虑下棋时能够考虑到棋局结束时情况,也就是说他能够将上图中的那棵博弈树在脑海中完全展开,看到叶子节点,那他就一定能够赢得这盘棋,因为他对于棋局的所有变化都可以知道,任何局面他都可以保证找到一步最佳着法。其实井字棋的博弈树既不烦琐也不深,所以整个树可以遍历展开,但如果是国际象棋,每一局都有35个左右的合理着法,即一个节点有35个子节点,第一层是35个局面,两层就有35*35个局面,六层就接近二十亿个局面,而十层就超过两千万亿个局面了。想要展开这棵博弈树,这是任何一位象棋大师也不可能做到的,甚至动用最强大的计算机也不可能做到。

我们在下棋时其实只会考虑几步之后的棋局,这个时候棋局没有结束,肯定是看不出来谁赢谁输的,但是我们可以估计谁最有可能赢,至少我们可以估计赢的概率大不大,容不容易赢。初学者可能只能看1,2步之后的棋局,而高手则可以看几步,甚至十几步之后的棋局,也就是说高手可以将博弈树展开的更大更深。在程序中,我们会用"评价函数"来估计棋局的好与坏,如果甲方赢棋或者很有希望赢,那么评价函数通常会返回正数;如果乙方是赢棋或者很有希望赢,那么返回负数;如果棋局是均势或者是和棋,那么返回在零左右的数值。

如下图所示,我们设定

评价值总是站在"×"的立场来看的,"×"总是希望棋局到达评价值大的局面,评价值越大,表明"×"越有优势,图中+1000表明优势最大("×"赢);而"Ο"总是希望棋局到达评价值小的局面,评价值越小,表明"×"越不利,其实就是"Ο"越有优势,图中-1000表明优势最小("×"输,即"Ο"赢)。

对于人机博弈的程序,程序主要趋向于遵循一个被称为极小极大的算法,又名MiniMax算法,是一种找出失败的最大可能性中的最小值的算法。该算法是一种零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,而另一方则选择令对手优势最小化的方法。如上图可见,可以通过树状搜索找到棋局中对双方来说都最好的着法,即是MinMax算法的关键。另外由于博弈树无法全部展开,我们只能展开部分博弈树,即我们只能向前查看几步后的棋局,因此在极小极大算法中我们限制对博弈树估算的深度。在很多运行在标准PC硬件的国际象棋程序中,极小极大搜索的深度被限制在6层左右——包含了十亿个可能的博弈位置。超过这个层数会导致的分析博弈位置的耗时更长,这是不现实的。例如,以1百万/s的比率分析博弈位置,6层的深度需要耗费约一刻钟。


极小极大算法及参考图示:

int MinMax(int depth)

{

if (SideToMove() =="×") {       // 如果轮到"×"走棋

return Max(depth);                 // "×"是先下棋者,取最大评估值

} else {             // 如果轮到"Ο"走棋

  return Min(depth);                 // 取最小评估值

 }

}

int Max(int depth) {                      //取最大评估值函数

 int best = -INFINITY;                 // 初始化最优值best = 负无穷大

 if (depth <= 0) {

  return Evaluate();                // 返回向前走depth步之后棋局的评价值

 }

GenerateLegalMoves();         // 生成当前"×"所有合理的走法

while (MovesLeft()) {              // 遍历"×"的所有走法

  MakeNextMove();               // 执行走法

val = Min(depth - 1);// 取"Ο"走子的最小评估值

  UnmakeMove();                 // 撤销走法

if (val > best) {                   // 获取"×"的最大评估值

   best = val;

  }

 }

 return best;                          // 返回best最大值

}

int Min(int depth) { // 取最小评估值函数

int best = INFINITY;            //初始化最优值best = 正无穷大,注意这里不同于“最大”算法

 if (depth <= 0) {

return Evaluate();// 返回向前走depth步之后棋局的评价值

 }

GenerateLegalMoves();// 生成当前"Ο"所有合理的走法

while (MovesLeft()) {  // 遍历"Ο"的所有走法

MakeNextMove();// 执行走法

val = Max(depth - 1);// 取"×"走子的最大评估值

UnmakeMove();// 撤销走法

if (val < best) {            //获取"Ο"的最大评估值,注意这里不同于“最大”算法

   best = val;

  }

 }

return best;// 返回best最小值

}


上面的代码可以这样调用:

val = MinMax(5);

这样可以返回当前局面的评价,它是向前看5步的结果。

上面的算法,大家可以参照下面的图加以理解。

图1(向前走2步的搜索树)

图2(向前走3步的搜索树) 

负值最大搜索:

负值最大只是对最小-最大的优化,它的代码更短,同时减少了移植代码时出错的可能,代码维护起来也比较方便。一个局面对"×"的优势为D,那么对"Ο"的优势就是-D;一个局面对"×"的优势为-D,那么对"Ο"的优势就是D。在负值最大算法中,没有了最小值,只有最大值,算法中用一个求最大值的操作代替了最小值和最大值交替的过程。需要注意的是,为了能使负值最大搜索算法得到正确的评价,必须修改局面评价函数的返回值,原来在极大极小搜索算法中始终返回的是"×"的优势,现在要改为当前走棋方的优势,占优返回正数,反之返回负数,最后这个值返回后必须加上负号,因为返回以后就是对另一方而言了。

负值最大算法及参考图示:

int NegaMax(int depth) {                   // 负值最大函数,求最大值的操作代替了最小值和最大值交替的过程

int best = -INFINITY;                      //初始化最优值best = 负无穷大

 if (depth <= 0) { 

return Evaluate();                     //返回向前走depth步之后,要走子一方的评价值

 }

GenerateLegalMoves();               //生成要走子一方所有合理的走法

while (MovesLeft()) {// 遍历要走子一方的所有走法

MakeNextMove();// 执行走法

val = -NegaMax(depth - 1);      // 获取另一方的评价值,注意这里有个负号

UnmakeMove(); // 撤销走法

if (val > best) {                         // 获取走子一方的最大评价值

   best = val;

  }

 }

return best;// 返回best最大值

}

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容