在上一篇我们分析了对抗搜索的基本思想,本文在上一篇的基础上对算法部分步骤进行优化。首先我们看一个基于对抗搜索建立的博弈树,对上篇的内容做一个简单的回顾。(本篇内容可以使用上篇开头说的两本书作为参考)
观察这颗博弈树,我们可知它的固定深度是3,MAX先执行。换句话来说就是,MAX为了决定第一步该走A、B还是C,它要从根节点开始遍历深度为3的子树。再换句话来说,这颗树其实只是为了获得MAX在第一步走的时候建立的博弈树;同样对于MIN方来说,假如MAX第一步选择走A,那么以A节点为根节点,同样建立一颗深度为3的子树,通过遍历该子树,获得MIN在这一步的最佳选择,A1、A2或者A3(未画出该图)。
通俗的说,对抗搜索其实就是在有限深度内枚举双发在每一步的选择,通过比较最终节点的状态(最大深度时)优劣,来获得此时执行方的最优选择。
回归到原图,对MAX的选择进行一个详细的分析(请结合上一篇的MAX_VALUE和MIN_VALUE代码):
- 第一步由MAX方执行,假设此时的节点状态为R,R有三个选择,即三个后继节点--A、B、C;R要在这三个选择中选出“最利于获胜”的节点作为自己的执行步骤,按照极大极小值方法,即max(A,B,C),此时深度为0;
- 接下来轮到MIN方执行,它要在R选择的步骤基础上,选择利于自己的最优步骤,此时深度为1;
- 接下来轮到MAX方执行,它同样在MIN选择的步骤基础上,选择利于自己的最优步骤,此时深度为2;
- 接下来本应轮到MIN方执行,由于它已经到达最大深度3,不再往下“递归”,即到达了该树的叶子节点,使用评价函数来计算当前叶子节点状态的值。可以看到每个叶子节点都由一个评估值。
- 然后从叶子节点开始往上回溯,看到在深度2时,MAX方在每一个选择下的最优选择;
- 继续往上回溯,此时回溯到MIN方的选择;
- 最终我们回溯到最初MAX此时的最优选择,选择C作为这一步的选择;
接下来我们思考一个问题,我们可以看到上述的算法是对所有的候选项都进行枚举遍历比较,这样如果深度比较大时,比如固定截取深度为10,那么对每一步来说都要计算最大深度为10的子树,这样的计算量会非常大。有没有可能缩小比较范围?
首先我们要清楚,max_value和min_value方法以DFS(深度优先)进行遍历。接下来我们对以A节点为根节点的子树进行详细分析。首先从深度3开始往上回溯时,此时A1的最优选择为6;此时再往上回溯,A节点此时的最优选择为6;然后开始遍历A2节点,因为A节点此时的值是6,如果A2节点能够得到该值的信息的话,那么在以A2节点为根的深度遍历时,遇到比6大的值,其实可以放弃该子树的后续遍历了。原因就是A2节点是MAX方来执行,如果A2的子节点有比6大的,那么MAX方肯定会选择比6大的值,而A节点是MIN方来执行,它要选择的是A1、A2、A3的最小值,所以此时A节点肯定不会选择A2作为最优选择。
同样,在A的选择结束后,向上回溯到R,此时R的最优选择是6,由于R要选取最大值,那么在对剩余以B和C为根节点进行深度遍历时,如果有遇到比6小的值,那么该节点肯定不是最优的选择。原因就是MAX方要选择后继节点中的最大值,比6小的话,肯定不是最优选择。
上述方法就是树结构最常用的优化方法--剪枝,这里的优化方法叫做Alpha-Beta方法。下面是通过Alpha-Beta剪枝方法对max_value方法和min_value方法的改进。
int max_value ( int dep , state s , int alpha , int beta ){
if ( terminal ( s )) return e ( s ); //终止状态
if ( dep == maxdepth ) return e ( s ); //深度截断,返回评价函数
v = - inf ; //初始化为负无穷
succ = make_successors ( s ); // succ [ i为第]个后继状态i
for ( i = 0; i < succ . count ; i ++){
v = max (v , min_value ( succ [ i ] , alpha , beta )); //计算所有儿子的最大值
if ( v >= beta ) return v ; //β剪枝
alpha = max ( alpha , v ); //更新α为最大值
}
return v ;
}
int min_value ( int dep , state s, int alpha, int beta){
if ( terminal ( s )) return e ( s ); //终止状态
if ( dep == maxdepth ) return e ( s ); //深度截断,返回评价函数
v = inf ; //初始化为无穷大
succ = make_successors ( s ); // succ [ i为第]个后继状态i
for ( i = 0; i < succ . count ; i ++){
v = min (v , max_value ( succ [ i ] , alpha , beta )); //计算所有儿子的最小值
if ( v <= alpha ) return v ; //α剪枝
beta = min ( beta , v ); //更新β为最小值
}
return v ;
}
接下来我们按照改进后的max_value方法和min_value方法对上述的博弈树进行剪枝,方便大家理解。
- 初始状态,alpha=-inf, beta=inf (inf代表最大值);通过深度优先遍历向下传递alpha, beta, 在到达叶子节点时,因为是计算评价函数alpha和beta其实没有用;然后开始向上回溯到A1节点;
- A1节点回溯前alpha=-inf, beta=inf,回溯后变为alpha=6, beta=inf;向上回溯到A节点;
- A节点回溯前alpha=-inf,beta=inf, 回溯后变为alpha=-inf, beta=6; 向下递归遍历A2节点;
- A2节点此时alpha=-inf, beta=6; 继续向下递归遍历,我们知道A2是max方来执行,此时调用的是max_value方法,并且有三个后继节点;
- 遍历第一个节点时,2<6, alpha = 2, beta=6;
- 遍历第二个节点时,8>6, 此时直接返回,也就是第三个节点不再遍历,因为max此时的选择肯定是大于6的,对A节点来说,A2节点肯定不是最优的选择;
- 此时向上回溯到A节点;
- 同样对于A3节点来说,在遍历完第一个节点7时,直接返回,继续回溯到A节点;
- 此时A节点的alpha = -inf, beta=6; 此时A节点向上回溯到R节点;
- R节点回溯前alpha=-inf,beta=inf; 回溯后变为alpha=6, beta=inf; R节点向下深度遍历到B节点;
- B节点alpha=6, beta=inf; 继续向下递归到B1节点;
- B1节点alpha=6, beta=inf; 继续向下递归遍历三个后继节点,三个节点的值都比beta小,所以只是在每个节点内部更改了alpha;最终回溯到B1节点;
- B1回溯前alpha=6, beta=inf, 回溯后alpha = 6, beta = inf; 接着向上回溯到B节点;
- B节点回溯前alpha=6, beta=inf, 此时回溯的值是5, 5<alpha=6, 直接回溯到R节点,也就是说直接裁剪了B2和B3节点值。原因: 因为B节点是由MIN方来执行,它要选择最小的节点作为最佳选择,所以不论B2和B3是什么值,对于B来说最佳选择肯定是小于等于5;而R是选择最大值,此时,R此时最优的选择是6,肯定不会选择B节点作为最优选择,所以对于B2和B3来说,已经没有继续搜索的必要了;接着向上回溯到R节点;
- R节点回溯前alpha=6, beta=inf; 向下递归遍历C节点;
- C节点alpha=6, beta=inf, 继续向下递归遍历C1;
- C1节点alpha=6, beta=inf, 继续向下递归遍历叶子节点, 由于都小于beta, 所以依然只是比较alpha的值,获取最大值向上回溯到C节点;
- C节点回溯前alpha=6,beta=inf, 回溯后alpha = 6, beta=7; 向下递归遍历C2节点;
- C2节点alpha=6,beta=7, 向下递归遍历叶子节点;
- 遍历第一个叶子节点4, 小于beta;
- 遍历第二个叶子节点8, 大于beta, 此时直接返回,裁剪了叶子节点1,直接向上回溯到节点C节点;
- C节点回溯前alpha = 6, beta=7;回溯后返回值为8大于beta, 依然选取最小值,alpha=6, beta=7;
- 对于C3节点与C2的处理方式相同,C3向上回溯到C节点;
- C节点回溯前alpha=6,beta=7,回溯后alpha=6,beta=7, 并选择最小值为7回溯到R节点;
- R节点回溯前alpha=6,beta=inf,回溯后alpha=7,beta=inf;
最终的裁剪图如下,标红的代表它的子树都被裁剪了。
总结
alpha代表的是MAX方选择的最小下界;
beta代表的是MIN方选择的最大上界;
来源:oschina
链接:https://my.oschina.net/u/3853930/blog/4318992