极大极小值算法( Minimax algorithm)
在上文的博弈树中,如果我们令甲胜的局面值为1,乙胜的局面值为-1,而和局的值为0。当轮到甲走时,甲定会选择子节点值最大的走法:而轮到乙时,乙则会选择子节点值最小的走法。所以,对于中间节点的值可以有如下计算方法:如果该节点所对应的局面轮到甲走棋,则该节点的值是其所有子节点中值最大的一个的值。而如果该节点所对应的局面轮到乙走棋,则该节点的值是其所有子节点中值最小的一个的值。
对博弈树的这个变化仅仅是形式上的,本质上丝毫未变,但是这个形式更容易推广以运用到一般实际的情形。
既然建立整棵的搜索树不可能,那么,为当前所面临的局面找出一步好棋如何?也就是通过少量的搜索,为当前局面选择一步较好的走法。
在通常的棋局当中,一个局面的评估往往并不像输、赢、平3种状态这么简单,在分不出输赢的局面中棋局也有优劣之分。也就是说,要用更细致的方法来刻画局面的优劣,而不是仅仅使用1、-1、0三个数字刻画3种终了局面。假定我们有一个函数可以为每一局面的优劣评分。例如甲胜为+∞:乙胜为-∞:和局为0:其他的情形依据双方剩余棋子的数量及位置评定-∞~+∞之间的具体分数。这样我们可以建立一棵固定深度的搜索树,其叶子节点不必是终了状态,而只是固定深度的最深一层的节点,其值由上述函数评出:对于中间节点,如同前面提到的那样,甲方取子节点的最大值,乙方取子节点的最小值。这个评分的函数称作静态估值函数( Static Evaluation function)。用以取代超出固定深度的搜索。显然,我们无法拥有绝对精确的静态估值函数。否则,只要这个静态估值函数就可以解决所有的棋局了。估值函数给出的只是一个较粗略的评分,在此基础上进行的少量搜索的可靠性,理论上是不如前述的WTN,LOST,DRAW三种状态的博弈树的,但这个方法却是可实现的。利用具体的知识构成评估函数的搜索叫做启发式搜索( Heuristic search)。估值函数在有些文献中也称为启发函数(Heuristic Function)
在博弈树搜索的文献当中,极大极小方法往往指的是基于静态估值函数的有限深度的极大极小搜索。在将来使用极大极小方法时如无特别说明也是指这种形式
深度优先搜索( Depth First Search)
在生成极大极小树并对其进行搜索的方法上,我们面临着多种选则
- 是先在内存中生成整棵树然后进行搜索,还是在搜索的过程中仅仅产生将要搜索的节
- 对于树的搜索以什么顺序进行,是广度优先( Breadth First Search)深度优先,还是其他顺序?
- 有必要生成整棵树吗?在搜索过程中将搜索过的节点删去行吗?
几乎所有的人在使用基本的极大极小算法时都选择了深度优先搜索方法。这样可以在搜索过程中的任何时候仅仅生成整棵树的一小部分,搜索过的部分被立即删去。显然,这个算法对内存的要求极低,往往在内存只有几千字节的机器上也可以实现。并且同其他的选择相比速度上也并不逊色。
深度优先搜索极大极小树的过程,可以表示为一个递归的形式。
如图所示的一棵树,共有3层。根节点为A,其子节点有B、C、D三个,而B、C、D也各有子节点若干。以深度优先算法搜索此树时,先进入根节点A,生成其第1个子节点B:然后遍历B,生成B的第1个子节点E;E将其估值返回给父节点B,删掉E,B生成第2个子节点F:F将其估值返回给父节点B,删掉F,B生成第3个子节点G;G将其估值返回给父节点B,删掉G,B在3个叶节点的返回值中取极小值并将此值返回给A, A生成其第2个子节点C:同样遍历C及其子节点,得到C的返回值后再生成D并向下遍历之;最后,A在B、C、D的返回值中取极大值,拥有该极大值的子节点就是下一步要走的方向。
深度优先算法实例
从上述过程可以看出,深度优先搜索极大极小树的过程中,任何时候只要保存与其层数相同个数的节点。在上例中,任何时刻仅需保存3个节点。仅生成将要搜索的节点,搜索完成的节点可以立即删去以节省空间。
用伪代码将深度优先搜索集大极小树算法描述如下:(伪代码仅仅是为说明算法,其内容是简略的)。
代码假定是用于中国象棋
int MiniMax(position p,int d)
{
int bestvalue,value;
if(Game Over) //检测棋局是否结束
return evaluation(p); //棋局结束返回估值
if(depth <= 0) //是否叶子节点
return evalueation(p); //叶子节点,返回估值
if(p.color == RED) //是否轮到红方走棋
bestvalue = -INFINITY; //是,领初始最大值为极小
else
bestvalue = INFINITY; //否,令初始最小值为极大
for(each possibly move m) //对每一个可能的走法m
{
MakeMove(m); //产生第i个局面(子节点)
value = MiniMax(p,d-1); //递归调用MiniMax向下搜索子节点
UnMakeMove(m); //恢复当前局面
if(p.color == RED)
bestvalue = max(value,bestvalue); //取最大值
else
bestvalue = min(value,bestvalue); //取最小值
}
return bestvalue; //返回最大/最小值
}
负极大值算法( Negamax Algorithm)
普通的极大极小值算法看起来有一点笨,既然一方试图取极大值而另一方试图取极小值——也就是说——我们总要检査哪一方要取极大值而哪一方又要取极小值,以执行不同的动作。Knth和More在1975年提出了负极大值( Negamax)方法①,消除了两方的差别,而且简洁优雅。使用负极大值方法,博弈双方都取极大值。
算法如下面的伪代码所示:
//类C伪代码,负极大值算法
int NegaMax(position p,int depth)
{
int n,value,bestvalue = -INFINITY; //最大值初始为负无穷
if(Game Over(p))
return evaluation(p); //胜负已分,返回估值
if(depth == 0) //叶子节点
return evaluation(p); //调用估值函数,返回估值
for(each possibly move m) //对每一个可能的走法
{
MakeMove(m); //产生新节点
value = -NegaMax(p,d-1); //递归搜索子节点
unMakemove(m); //撤销新节点
if(value >= bestvalue)
bestvalue = value; // 取最大值
}
return bestvalue; //返回最大值
}
可以看出,负极大值算法比极大极小值算法短小并且简单。关键的不同在于
value= -NegaMax( p, d-1)
注意其中的负号。负极大值算法的核心在于:父节点的值是各子节点的值的负数的极大值。如要这个算法正确运作,还要注意一点额外的东西。例如象棋,估值函数必须对谁走棋敏感,也就是说对于一个该红方走棋的局面返回正的估值的话,则对于一个该黑方走棋的局面返回负的估值
初看上去,负极大值算法比极大极小值算法稍难理解,但事实上负极大值算法更容易被使用。在算法的原理上,这两种算法完全等效。负极大值算法仅仅是一种更好的表达形式。今天的博弈程序大多采用的也都是基于负极大值形式的搜索算法。本书的例子也不例外。
内容来源于:陈其的《PC游戏编程》(人机博弈)