| 4.5 候補手の並び替え |
αβ法では、評価値の高い(相手局面では評価値の低い)ノードから探索を行なうと探索ノード数を減らすことができます。
評価値の高いノードの探索を行うと、探索の下限が上昇します。
その結果、探索の上限と下限の幅が狭くなります。
探索の上限と下限の幅が狭い方が枝刈りが多く発生するので、探索ノードが減ることになります。
本節では、探索時に候補手に対して1手読みを行い、その評価値を基に候補手の並び替えを行ないます。
候補手の並び替えを行なうことで評価値の高そうなノードから探索を行なうことができます。
まず着手箇所(実際に使用するのは候補手リストの要素)と評価値を格納するための構造体を定義します。
typedef struct _MoveInfo MoveInfo;
struct _MoveInfo
{
MoveList *Move;
int Value;
};
次に候補手の並び替えを行なう関数を実装します。
static int Com_Sort(Com *self, int in_color, MoveInfo *out_info)
{
int info_num = 0;
MoveList *p;
MoveInfo info_tmp, *best_info;
int i, j;
for (p = self->Moves->Next; p; p = p->Next) {
if (Board_Flip(self->Board, in_color, p->Pos)) {
out_info[info_num].Move = p;
out_info[info_num].Value = Evaluator_Value(self->Evaluator, self->Board);
info_num++;
Board_Unflip(self->Board);
}
}
if (in_color == WHITE) {
for (i = 0; i < info_num; i++) {
out_info[i].Value = -out_info[i].Value;
}
}
for (i = 0; i < info_num; i++) {
best_info = &out_info[i];
for (j = i + 1; j < info_num; j++) {
if (out_info[j].Value > best_info->Value) {
best_info = &out_info[j];
}
}
info_tmp = *best_info;
*best_info = out_info[i];
out_info[i] = info_tmp;
}
return info_num;
}
引数は以下の通りです。
self : Comクラスへのポインタ
in_color : 手番
out_info : 並び替え結果を格納するMoveInfo構造体へのポインタ
処理は3つに分かれており、それぞれ以下のようになっています。
ソート方法には選択ソートを使用しました。
それでは候補手の並び替えを探索処理に導入します。
Com_MidSearch()を以下のように修正しました。
static int Com_MidSearch(Com *self, int in_depth, int in_alpha, int in_beta, int in_color, int in_opponent, int in_pass, int *out_move)
{
MoveList *p;
int value, max = in_alpha;
int can_move = 0;
int move;
/* 並び替え用の着手情報 */
MoveInfo info[BOARD_SIZE * BOARD_SIZE / 2];
int i, info_num;
if (in_depth == 0) {
self->Node++;
return Evaluator_Value(self->Evaluator, self->Board);
}
*out_move = NOMOVE;
/* 残り手数が2手より多いときには候補手の並び替えを行なう */
if (in_depth > 2) {
info_num = Com_Sort(self, in_color, info);
if (info_num > 0) {
*out_move = info[0].Move->Pos;
can_move = 1;
}
for (i = 0; i < info_num; i++) {
Board_Flip(self->Board, in_color, info[i].Move->Pos);
RemoveList(info[i].Move);
value = -Com_MidSearch(self, in_depth - 1, -in_beta, -max, in_opponent, in_color, 0, &move);
Board_Unflip(self->Board);
RecoverList(info[i].Move);
if (value > max) {
max = value;
*out_move = info[i].Move->Pos;
if (max >= in_beta) {
return in_beta;
}
}
}
} else {
for (p = self->Moves->Next; p; p = p->Next) {
if (Board_Flip(self->Board, in_color, p->Pos)) {
RemoveList(p);
if (!can_move) {
*out_move = p->Pos;
can_move = 1;
}
value = -Com_MidSearch(self, in_depth - 1, -in_beta, -max, in_opponent, in_color, 0, &move);
Board_Unflip(self->Board);
RecoverList(p);
if (value > max) {
max = value;
*out_move = p->Pos;
if (max >= in_beta) {
return in_beta;
}
}
}
}
}
if (!can_move) {
if (in_pass) {
*out_move = NOMOVE;
self->Node++;
max = DISK_VALUE * (Board_CountDisks(self->Board, in_color) - Board_CountDisks(self->Board, in_opponent));
} else {
*out_move = PASS;
max = -Com_MidSearch(self, in_depth - 1, -in_beta, -max, in_opponent, in_color, 1, &move);
}
}
return max;
}
探索の残り手数が2手を超えていれば並び替えを行うようにしました。
リーフ近くで並び替えをしてしまうと、並び替えの処理時間が長くなってしまいます。
そのため比較的浅いノードでのみ並び替えを行なうようにします。
並び替えを行なった後の処理は、並び替えを行なわない場合の処理と同様です。
Com_EndSearch()では残り手数が8手を超えている場合に並び替えを行なうようにしました。
Com_EndSearch()の修正はCom_MidSearch()とほぼ同じなので省略します。
それでは実際に動作させてみましょう。
並び替えを行なう分1ノードあたりの処理時間が長くなってしまいました。
しかし探索ノード数が大きく減っているために、全体としては高速になっているのがわかると思います。