本质上和Alpha-Beta算法一样,但不以"极大-极小搜索算法"为出发点。与树型图的结合更加紧密。
一. 搏弈树
红棋走一步后,黑棋有多种应对招法。黑棋走完后,红棋又有多种走法可选。依次类推,就构成了一个搏弈树。
【 图1 】
二. 静态评分
如图1所示,在某个点上,不考虑后续步法,仅就双方当前形势进行评分。评分以棋子质量为基础。如果该点是红方走的棋,得分为红方质量为r减去黑方质量为b (r-b)。如果该点是黑方走的棋,分值为b-r。
(除质量外,还可以考虑棋子的攻击力、防御力、敏捷性等因素。我使用的是JS语言,只考虑棋子质量,这样性价比高)
三. 动态评分
受计算机性能限制,搏弈树的层次是有限的。如果不考虑水平线效应【注1】,在叶子结点,静态分值就是其最终得分。在非叶子节点,父结点分值等于最大子节点分值的相反数。比如某点是红方棋,静态分值为2。接下来黑棋有4种应对走法,静态分值依次是-1、0、1,3。不难理解,与前面的2分相比,后面的-max(-1,0,1,3) = -3 更准确反映出前一步红棋的优劣。同样道理,黑棋的4个分值也可再递归下去,层数越多,结果越准确。
四. 剪枝原理
如图1所示,已知G点得分为-9,K点得分为10。根据-max方法,H点得分将小于等于-10,H肯定不如G好,那么L点及其后续 节点就没必要再搜索了。left-upperleft搜索就是基于这么简单的原理。
五. 伪码示例
/*
* depth:当前深度
* upperLeft:当前节点"大爷"的分值
* left:当前节点"哥哥"的分值
*/
function dynamism(depth,upperLeft,left)
{
if(depth >= MAXDEPTH){
return quiescence();//静态评分函数
}
else{
var arr = getMoves();//获取行棋方着法
var val = upperLeft;
for(var i=0;i<arr.length;++i){
var v = dynamism(depth+1,left,val);
if(v >= -left){ // 剪枝!
return -v;
}
if(v > val){
val = v;
}
}
return -val;
}
}
假设我们要用该方法获取图1中H点动态评分,那么G点得分作为left值,B点得分作为upperLeft值。
参数left的意义:
当前节点得分不能低于left值(如果哥哥更好,自己就没存在必要了)。遍历当前点的子节点,它们得分不能高于-left,否则触发剪枝。
参数upperLeft的意义:
当前点得分不能大于-upperLeft(否则可证明父节点是个失败着法,自己也就必要继续存在了)。为了不让自己的祖辈失败,子节点不能低于某个分值。比如搜索H点时,其upperLeft为8(B点得分)。如果H点得分大于-8则C点失败。为了不让C点失败,K或者L的值大于8才是有效的。
六. 初始参数
dynamism(0,-9999,-9999);
因为当前点的值不小于left,所以left初始值赋以负极大值。因为当前点的值不大于-upperLeft,所以upperleft初始赋负极大值。
七. 与alpha-beta算法的关系
beta相当于-left,alpha相当于upperLeft。alpha-beta算法内循环过程中分值取反,但最终返回值不取反。另外depth采用递减方式。
注1:水平线效应
搏弈树上,叶子结点采用静态评分,非叶子结点采用动态评分。在叶子结点,如果某步棋吃掉了对方一子,那么走棋方会在质量上暂时取得较大优势。但下一步对方很可能再吃回来甚至取得更大优势。如果交替吃下去双方分值将反复震荡,称为水平线效应。水平线效应是静态评分函数要克服的最大困难。为了克服水平线效应,静态评分函数实际上也是递归调用的。
参考文章: