搜索完子节点时,需要更新当前节点处的信息。不妨假设当前节点 X 是 MAX 节点,且刚刚搜索完它的子节点 Y。那么,节点 X 处的 β 值不会改变,只有 α 值需要与子节点 Y 的分数取最大值。如果子节点 Y 是叶子节点,直接用子节点 Y 的分数更新当前节点 X 处的 α 值;否则,只需要用子节点 Y 的 β 值更新当前节点 X 的 α 值。此时,有三种可能性:
子节点 Y 的 β 值严格位于节点 X 的 α 值和 β 值之间。因为子节点 Y 继承了节点 X 的 α 值且不会更新它,所以,搜索子节点 Y 完后仍然有 β>α,就说明搜索子节点 Y 时没有发生剪枝。子节点 Y 最终的 β 值,就等于它继承的节点 X 的 β 值和它(指子节点 Y)的所有子节点的分数中,最小的那个。既然这个最小值严格小于节点 X 的 β 值,就说明它一定是子节点 Y 的所有子节点的分数最小值。因此,作为 MIN 节点,子节点 Y 的分数就是这个 β 值。用它更新节点 X 的 α 值是合理的。
子节点 Y 的 β 值就等于节点 X 的 β 值。如上文所述,这说明子节点 Y 的所有子节点的分数均不小于节点 X 的 β 值。这进一步说明 Beta 玩家不会任由局面进入节点 X:因为 Alpha 玩家只要选择了子节点 Y,Beta 玩家就不能取得比 β 更低的分数。因此,此时使用子节点 Y 的 β 值更新节点 X 的 α 值,是为了使得节点 X 处 α=β,以触发剪枝条件。它的效果与使用 Y 处实际分数——一个大于等于节点 X 处 β 值的数字——更新节点 X 的 α 值的效果是一样的。
子节点 Y 的 β 值小于等于节点 X 的 α 值。此时,子节点 Y 触发了剪枝条件,它的实际分数不会超过子节点 Y 的 β 值,更不会超过节点 X 的 α 值。用子节点 Y 的实际分数更新节点 X 的 α 值不会改变 α 值。这与使用子节点 Y 的 β 值更新节点 X 的 α 值的效果是一样的。
这一分析说明,当某个子节点搜索完成后,只有它的分数处于第一种情形时,α(或 β)才准确记录了这个子节点作为一个 MAX 节点(或 MIN 节点)的实际分数。对于其他情形,虽然它未必是准确的分数,但是它提供的信息足以保证剪枝的正确进行,从而不影响根节点处的分数记录。
搜索到节点 A 时,由于左子节点的分数为 3,而节点 A 是 MIN 节点,试图找分数小的走法,于是将 β 值修改为 3,这是因为 3 小于当前的 β 值(β=+∞)。然后节点 A 的右子节点的分数为 17,此时不修改节点 A 的 β 值,这是因为 17 大于当前的 β 值(β=3)。此时,节点 A 的所有子节点已搜索完毕,即可计算出节点 A 的分数为 3,这与该节点处记录的 β 值一致(前文的情形 1)。
节点 A 是节点 B 的子节点,计算出节点 A 的分数后,可以更新节点 B 的 α 和 β 值。由于节点 B 是 MAX 节点,试图找分数大的走法,于是将 α 值修改为 3,这是因为子节点 A 处的 β 值(β=3)大于当前的 α 值(α=−∞)。之后,搜索节点 B 的右子节点 C,并将节点 B 的 α 和 β 值传递给节点 C。
对于节点 C,由于左子节点的分数为 2,而节点 C 是 MIN 节点,于是将 β 值修改为 2。此时 α≥β,故节点 C 的剩余子节点就不必搜索了,因为可以确定,Alpha 玩家不会允许局面发展到节点 C。此时,节点 C 是 MIN 节点,它的分数就是 2,不超过记录的 β 值(前文的情形 3)。由于节点 B 的所有子节点搜索完毕,即可计算出节点 B 的分数为 3,与记录的 α 值相同(前文的情形 1)。
计算出节点 B 的分数后,节点 B 是节点 D 的一个子节点,故可以更新节点 D 的 α 和 β 值。由于节点 D 是 MIN 节点,于是将 β 值修改为 3。然后节点 D 将 α 和 β 值传递给节点 E,节点 E 又传递给节点 F。对于节点 F,它只有一个分数为 15 的子节点,由于 15 大于当前的 β 值,而节点 F 为 MIN 节点,所以不更新其 β 值,然后可以计算出节点 F 的分数为 15,大于记录的 β 值(前文的情形 2)。
计算出节点 F 的分数后,节点 F 是节点 E 的一个子节点,故可以更新节点 E 的 α 和 β 值。节点 E 是 MAX 节点,更新 α 值,此时 α≥β,故可以剪去节点 E 的余下分支(即节点 G)。然后,节点 E 是 MAX 节点,将节点 E 的分数设为 15,严格大于记录的 α 值(前文的情形 3)。利用节点 E 的 α 值更新节点 D 的 β 值,仍然是 3。此时,节点 D 的所有子节点搜索完毕,即可计算出节点 D 的分数为 3,等于记录的 β 值(前文的情形 1)。
计算出节点 D 的分数后,节点 D 是节点 H 的一个子节点,故可以更新节点 H 的 α 和 β 值。节点 H 是 MAX 节点,更新 α。然后,按搜索顺序,将节点 H 的 α 和 β 值依次传递给节点 I、J、K。对于节点 K,其左子节点的分数为 2,而节点 K 是 MIN 节点,更新 β,此时 α≥β,故可以剪去节点 K 的余下分支。然后,将节点 K 的分数设为 2,小于等于记录的 β 值(前文的情形 3)。
计算出节点 K 的分数后,节点 K 是节点 J 的一个子节点,故可以更新节点 J 的 α 和 β 值。节点 J 是 MAX 节点,更新 α,但是,由于节点 K 的分数小于 α,所以节点 J 的 α 值维持 3 不变。然后,将节点 J 的 α 和 β 值传递给节点 L。由于节点 L 是 MIN 节点,更新 β=3,此时 α≥β,故可以剪去节点 L 的余下分支。由于节点 L 没有余下分支,所以此处并没有实际剪枝。然后,将节点 L 的分数设为 3,它小于等于记录的 β 值(前文的情形 3)。
计算出节点 L 的分数后,节点 L 是节点 J 的一个子节点,故可以更新节点 J 的 α 和 β 值。节点 J 是 MAX 节点,更新 α,但是,由于节点 L 的分数小于等于 α,所以节点 J 的 α 值维持 3 不变。此时,节点 J 的所有子节点搜索完毕,即可计算出节点 J 的分数为 3,它等于记录的 α 值(前文的情形 2)。
计算出节点 J 的分数后,节点 J 是节点 I 的一个子节点,故可以更新节点 I 的 α 和 β 值。节点 I 是 MIN 节点,更新 β,此时 α≥β,故可以剪去节点 I 的余下分支。值得注意的是,由于右子节点的存在,节点 I 的实际分数是 2,小于记录的 β 值(前文的情形 3)。
计算出节点 I 的分数后,节点 I 是节点 H 的一个子节点,故可以更新节点 H 的 α 和 β 值。节点 H 是 MAX 节点,更新 α,但是,由于节点 I 的分数小于等于 α,所以节点 H 的 α 值维持 3 不变。此时,节点 H 的所有子节点搜索完毕,即可计算出节点 H 的分数为 3,它等于记录的 α 值(前文的情形 1)。
这就是最终结果。
实现
参考代码
int alpha_beta(int u, int alph, int beta, bool is_max) { if (!son_num[u]) return val[u]; if (is_max) { for (int i = 0; i < son_num[u]; ++i) { int d = son[u][i]; alph = max(alph, alpha_beta(d, alph, beta, !is_max)); if (alph >= beta) break; } return alph; } else { for (int i = 0; i < son_num[u]; ++i) { int d = son[u][i]; beta = min(beta, alpha_beta(d, alph, beta, !is_max)); if (alph >= beta) break; } return beta; }}