一般来说,程序搜索得越深,程序的棋力就越强。如果搜索深度足于算到终盘,就能得到真正的最佳棋步和精确比分。但是随着搜索深度的增加,搜索所花费的时间也将急剧上升。而在现实下棋或比赛中,时间往往是有限的。比如在常规黑白棋比赛中,一般限定每一方的总用时为20分钟。由于受到时间的制约,程序的搜索深度无法事先确定,而需要在下棋过程中动态地进行调整。为此,程序在下每步棋时,可以根据己方剩余时间的多少,合理分配当前这步棋的思考时间。但由于程序软硬件环境的不同和局面的不断变化,搜索深度和搜索时间之间没有一成不变的关系,搜索深度仍然无法确定。前面所介绍的固定深度的搜索已无法适用,这时可以采用迭代加深搜索(Iterative Deepening Search)。迭代加深搜索的代码如下:
…… // 如果进一步搜索的深度未超过剩余空位数 while (++depth <= n_empty) { // 进一步搜索 value = alpha_beta(-INF_VALUE, INF_VALUE, depth); // 如果超时 if (TIME_OUT) break; } ……
为了保存所搜索到的最佳棋步结果,我们还引入了全局变量result。
// 无穷大分值 #define INF_VALUE 10000 // 初始搜索深度 #define MIN_DEPTH 4 // 测试是否超时 #define TIME_OUT (stop_clock && clock() >= stop_clock) // 最佳棋步结果 static int result; // 搜索终止时刻 static int stop_clock; int alpha_beta(int alpha, int beta, int depth, int pass = 0){ // 当前最佳棋步 int best_move = PASS; // 当前最佳分值,预设为负无穷大 int best_value = -INF_VALUE; // 尝试每个下棋位置 for (int pos=A1; pos<=H8; ++pos) { // 试着下这步棋,如果棋步合法 if (make_move(pos)) { int value; // 如果到达预定的搜索深度,直接给出局面估值 if (dept <= 1) value = -evaluation(); // 否则,对所形成的局面进行递归搜索 else value = -alpha_beta(-beta, -alpha, depth - 1); // 撤消棋步 undo_move(pos); // 如果超时 if (TIME_OUT) return -INF_VALUE; // 如果这步棋引发剪枝 if (value >= beta) { // 立即返回当前最佳结果 result = pos; return value; } // 如果这步棋更好 if (value > best_value) { // 保存更好的结果 best_move = pos; 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(); } } // 返回最佳结果 result = best_move; return best_value; } int ids(int time_remain){ // 开始时刻 int start_clock = clock(); // 初始搜索深度 int depth = MIN_DEPTH; // 初始搜索不限时,以确保得到一个正确的棋步 stop_clock = 0; // 进行搜索深度 int best_value = alpha_beta(-INF_VALUE, INF_VALUE, depth); int best_move = result; // 如果进一增加搜索深度步搜索的深度未超过剩余空位数 while (++depth <= n_empty) { // 根据已进行的搜索情况重新调整搜索终止时刻 stop_clock = start_clock + (clock_t)(time_remain * (CLOCKS_PER_SEC / 1000.) - (clock() - start_clock)) / ((n_empty - depth + 2) / 2); // 进行常规的剪枝算法 int value = alpha_beta(-INF_VALUE, INF_VALUE, depth); // 如果超时 if (TIME_OUT) break; // 更新最佳结果 best_move = result; best_value = value; } // 返回最佳结果 result = best_move; return best_value; }
迭代加深搜索的思路十分简单,先从很浅的搜索深度(比如初始搜索深度为1)开始进行搜索,然后逐渐加深搜索深度进行下一轮搜索,直至时间用完为止。这样就可以做到有多少时间,就搜索多少深度。当然,上面这段代码只是个示意,在实际应用中,alpha_beta()内部也应进行超时判断。如果在某一深度的搜索过程中时间用完,搜索应立即中止,以免因等待本轮搜索结束而造成时间耗尽。
你也许会注意到,只有最后一轮搜索才是真正需要的,在这之前的搜索似乎都是在浪费时间。事实上,程序的搜索深度每增加一层,搜索时间往往成倍增长。程序大部分的时间将花在最后一轮搜索上,之前的搜索时间基本可以忽略。而且,迭代加深搜索还可以与散列表结合使用,这样较浅度搜索的结果将保存在散列表中,可对更深度搜索起一定的指导作用。如果采用MTD(f)算法,前一次搜索的结果还可以做为更深一层搜索的初始试探值:
int deepening() { int value = 0; // 初始搜索深度 int depth = 1; do { // 进行常规的MTD(f)算法 value = mtd(value, depth); // 增加搜索深度 depth++; // 直到时间用完 } while (!time_out) ; return value; }
实践表明,迭代加深搜索所花费的时间只比固定深度的搜索略微多一点;更重要的是,这种算法可以适用于信赖时间进行搜索的场合。
在对每步棋思考时间的分配上,也有不少文章可做。例如前面所提到的,当某一深度的搜索在进行中发生超时,这一轮搜索将强行中止。由于搜索过程不完整,其结果往往需丢弃,这就难免会造成一定的浪费。一个解决此问题的思路是,虽然每一轮搜索所需的时间无法预先确定,但搜索过程还是有一定规律可循的。根据前几轮搜索的情况,可以大致预测出本轮搜索的时间。如果预测表明,剩余时间已经不足于完成本轮的搜索,那么就不再继续进行本轮搜索,而把剩余的时间留给下一步棋。