α-β剪枝算法

前面介绍的基本搜索算法,在实际应用是是十分费时的,因为它需要考虑所有可能的棋步。有研究表明,在黑白棋的中盘阶段,平均每个局面大约有10步棋可供选择[1]。如果程序前瞻10步(搜索深度为10),就需要考虑大约100亿个局面。假设计算机以每秒1000万个局面的速度进行运算,每下一步棋大约需要运算十几分钟。因此,在有限的时间内,程序无法进行很深的搜索,这就大大制约了程序的棋力。

有没有更高效的搜索方法呢?Edwards、Timothy(1961年)[2]、Brudno(1963年)[3]等人相继在研究中发现,程序搜索过程中有很多局面是完全可以忽略的,并提出了α-β剪枝算法(Alpha-beta Pruning)。我们就仍以图1所示的局面为例,简要说明剪枝算法的原理。

算棋局面
图1 白先,当前最佳估值为0

假设白棋已经搜索了D6的后续变化,得出这步棋的估值为0。接着开始搜索F4这步棋,白棋下F4后形成图2所示的局面。

出现剪枝
图2 黑先,当前最佳估值为+6

在这一局面中,黑棋相继搜索了C3、D3、E3三步棋,当前最佳估值是E3的+6。作为黑棋方,他是很乐意看到有E3这样的好棋,他也很希望尚未进行搜索的F3和G3能有更好的表现。但问题是,黑棋能遇上E3这样好棋的前提是白棋要先下出F4,下F4的决定权在于白棋方。而对于白棋而言,黑棋的这步E3回应显然对他太不利了。在他看来,F4这步棋的估值最多只有-6,甚至有可能更差。当白棋知道黑棋将有一步好棋E3在等着他时,他是绝对不会去下F4的,因为他完全可以选择下D6,或者等待后续搜索中可能出现的更好棋步。换句话说,黑棋根本没有机会面对图2所示的局面,F3和G3这两步棋的结果已经无关紧要了,黑棋没有必要再继续搜索下去。我们将这种现象称为剪枝(Pruning)。

这看上去是个相当诡异的现象,黑棋从自己的利益出发,努力寻找尽可能好的棋步,但是一旦他的棋步好过头了,对于当前局面的搜索工作就瞬间变成是多余的。黑棋搜索过程中这种微妙的界限就来自于上一层白棋的当前估值下限,考虑到下棋方轮换所带来的影响,白棋当前的估值下限通过取负值后,传递给下一层黑棋作为估值上限。我们将估值下限记为α,将估值上限记为β。我们只要将基本搜索算法稍加修改,就能得到α-β剪枝算法:

int alpha_beta(int alpha, int beta, int depth, int pass = 0){
	// 当前最佳分值,预设为负无穷大
	int best_value = -INF_VALUE;
	// 尝试每个下棋位置
	for (int pos=A1; pos<=H8; ++pos) {
		// 试着下这步棋,如果棋步合法
		if (make_move(pos)) {
			int value;
			// 如果到达预定的搜索深度,直接给出局面估值
			if (depth <= 1) value = -evaluation();
			// 否则,对所形成的局面进行递归搜索
			else value = -alpha_beta(-beta, -alpha, depth - 1);
			// 撤消棋步
			undo_move(pos);
			// 如果这步棋引发剪枝
			if (value >= beta) {
				// 立即返回当前最佳结果
				return value;
			}
			// 如果这步棋更好
			if (value > best_value) {
				// 保存更好的结果
				best_value = value;
				// 更新估值下限
				if (value > alpha) alpha = value;
			}
		}
	}
	// 如果没有合法棋步
	if (best_value == -INF_VALUE) {
		// 如果上一步棋也是跳步,表明对局结束
		if (pass) {
			// 直接给出精确比分
			best_value = get_score();
		// 否则这步棋跳步
		} else {
			make_pass();
			// 递归搜索,并标明该跳步
			best_value = -alpha_beta(-beta, -alpha, depth, 1);
			// 撤消跳步
			undo_pass();
		}
	}
	// 返回最佳结果
	return best_value;
}

我们引进了估值上、下限参数alpha和beta,它们在递归运算过程中逐层向下传递。上一层搜索的上限和下限在取负值后,分别成为下一层搜索的下限和上限。每当搜索到的估值触及当前搜索层的估值上限beta值时,就引发剪枝。

这里我们注意到,估值下限α值也是由上一层搜索的估值上限取负值而来,其目的是使更下一层的搜索加速引发剪枝。而这样做所带来的“副作用”是,由于在下一层提前剪枝,有可能返回被高估的估值(因为下一层剪枝时被低估了)。但进一步研究表明,如果当前局面存在更好的估值,此高估的估值将被取代;而如果此高估的估值就是当前局面的最佳估值,由于它不会超过估值下限α值(也就是上一层估值上限的负值),它必将在上一层引发剪枝;在这两种情况下,估值是否被高估都将带来相同的结果。当然,这仅仅是原理性的说明,高德纳与Moore(1975年)[4]已经严格证明了α-β剪枝算法的正确性。

从上面的分析可以看出,α-β剪枝算法真正关注的是(α, β)区间内的估值。而对于区间之外的估值,由于会引发剪枝,所得到的估值并不一定是其实际值。因此在搜索的最顶层,为了得到准确的估值,搜索区间应选取最大可能的估值区间,例如:(-64, 64)。而在那些只需知道胜负结果、而无需知道准确比分的场合,可以选取(-1, 1)作为(α, β)区间。

由于存在剪枝情况,程序真正需要搜索的局面数量将大大减少。实践表明,加入α-β剪枝算法后,同样是中盘向前搜索10步的情况,程序平均每步棋只需考虑100万数量级的局面。相对于没有α-β剪枝时的100亿个局面来说,搜索效率明显提高。正是由于这种高效性,当今强大的黑白棋程序(也包括其他的棋类程序),都无不例外地采用α-β剪枝算法及其变形,并在此基础上进行改进、增强。

[1] Louis Victor Allis. Searching for Solutions in Games and Artificial Intelligence. University of Limburg, Maastricht, The Netherlands, 1994, ISBN 90-9007488-0.

[2] Daniel Edwards, Timothy Hart. The Alpha-Beta Heuristic (AIM-030). Massachusetts Institute of Technology, 1961.

[3] T.A. Marsland. Computer Chess Methods. J. Wiley & Sons., 1987, 5:159–171.

[4] Donald E. Knuth, Ronald W. Moore. An Analysis of Alpha-Beta Pruning. Artificial Intelligence, 1975, 6(4): 293-326.