思うだけで学ばない日記 2.0

思うだけで学ばない日記から移転しました☆!よろしくお願いします。

PV表示はいろんなことを教えてくれる…かもしれない

記憶モードだがWikipediaのネガアルファ法のページにはこんな感じのコードが載っていた。
これは評価値しか出力しないが、正しく動く

template<int end_ab>
int NegaAlpha(int rdepth, int alpha, int beta)
{
    EnumContext& node = nodes[rdepth];

    if (rdepth <= 0) {
        int v = (end_ab != 0)
              ?  node.GetEvalOnA(alpha, beta)
              : -node.GetEvalOnA(-beta, -alpha);
        return v;
    }

    while (node.MoveNext()) {
        TPMove curMove = node.GetLastMove();
        int v = -NegaAlpha<end_ab>(rdepth - 1, -beta, -alpha);  // (1)
        if (v > alpha) {
            alpha = v;
            if (alpha >= beta) {
                node.Reset();
                return alpha;
            }
        }
    }
    node.Reset();

    return alpha;
}

int Search(int sdepth, int end_ab)
{
    return (end_ab != 0) ? NegaAlpha<1>(sdepth, -INFINITE, INFINITE)
                         : NegaAlpha<0>(sdepth, -INFINITE, INFINITE);
}

これに能天気なGMA0BNが天の啓示を受けてPVの記録コードを追加しようとした↓

template<int end_ab>
int NegaAlpha(int rdepth, int alpha, int beta, TPVElem* ppv)
{
    EnumContext& node = nodes[rdepth];
    ppv = pv_alloc(0);

    if (rdepth <= 0) {
        int v = (end_ab != 0)
              ?  node.GetEvalOnA(alpha, beta)
              : -node.GetEvalOnA(-beta, -alpha);
        return v;
    }

    while (node.MoveNext()) {
        TPMove curMove = node.GetLastMove();
        TPVElem* ppv2;
        int v = -NegaAlpha<end_ab>(rdepth - 1, -beta, -alpha, ppv2);
        if (v > alpha) {
            alpha = v;
            if (alpha >= beta) {   // βカット
                node.Reset();
                return alpha;
            }
            pv_free(ppv);
            ppv = pv_alloc(curMove);
            ppv = pv_join(ppv, ppv2);
        } else {
            pv_free(ppv2);
        }
    }
    node.Reset();

    return alpha;
}

int Search(int sdepth, int end_ab, TPVElem* ppv)
{
    return (end_ab != 0) ? NegaAlpha<1>(sdepth, -INFINITE, INFINITE, ppv)
                         : NegaAlpha<0>(sdepth, -INFINITE, INFINITE, ppv);
}

だがこれではたまに正常に動かないのじゃ
なぜなら、上のコードではβカットされたらPV記録しない仕様だが、このロジック一本槍ではうまくいかないケースが存在する。
すわなち、相手を詰ませたとき、Search()→NegaAlpha()→-NegaAlpha()の最後のNegaAlpha()が -INFINITEを返すが、すると上記コードのβカット判定if文において、INFINITE >= INFINITEということで必ずβカット様の動きになってしまう。すると、相手を詰ませたのだから本当は勝ち確定だからPVとしては大いにアリなのに、ppvには何も残らない。最善手をPVの第一手から持ってくるような作りにした場合、勝ち確定でいきなり投了するからびっくりだ

そもそも理論上は探索の根ノードでβカットが起きるはずは無い(∵自明)*1
というわけで、こうしたら良くなった↓*2

template<bool root, int end_ab>
int NegaAlpha(int rdepth, int alpha, int beta, TPVElem* ppv)
{
    EnumContext& node = nodes[rdepth];
    ppv = pv_alloc(0);

    if (rdepth <= 0) {
        int v = (end_ab != 0)
              ?  node.GetEvalOnA(alpha, beta)
              : -node.GetEvalOnA(-beta, -alpha);
        return v;
    }

    while (node.MoveNext()) {
        TPMove curMove = node.GetLastMove();
        TPVElem* ppv2;
        int v = -NegaAlpha<false, end_ab>(rdepth - 1, -beta, -alpha, ppv2);
        if (v > alpha) {
            alpha = v;
            if (!root && alpha >= beta) {   // βカット;ただし根ノードではβカットは有り得ない
                node.Reset();
                return alpha;
            }
            pv_free(ppv);
            ppv = pv_alloc(curMove);
            ppv = pv_join(ppv, ppv2);       // まだまだ飛べるぜよ!
        } else {
            pv_free(ppv2);
        }
    }
    node.Reset();

    return alpha;
}

int Search(int sdepth, int end_ab, TPVElem* ppv)
{
    return (end_ab != 0) ? NegaAlpha<true, 1>(sdepth, -INFINITE, INFINITE, ppv)
                         : NegaAlpha<true, 0>(sdepth, -INFINITE, INFINITE, ppv);
}

だがこれでもまだ問題がある。このコードでは、Search()→NegaAlpha()→-NegaAlpha()の最後のNegaAlpha()が-INFINITEを返しても、もう起こりえないalphaの更新を求めて最後までループを回ろうとする。
勝ち確定したのにその後も延々考え続けてtimeupして負けるからびっくりだ*3

これが正解らしいいい↓

template<bool root, int end_ab>
int NegaAlpha(int rdepth, int alpha, int beta, TPVElem* ppv)
{
    EnumContext& node = nodes[rdepth];
    ppv = pv_alloc(0);

    if (rdepth <= 0) {
        int v = (end_ab != 0)
              ?  node.GetEvalOnA(alpha, beta)
              : -node.GetEvalOnA(-beta, -alpha);
        return v;
    }

    while (node.MoveNext()) {
        TPMove curMove = node.GetLastMove();
        TPVElem* ppv2;
        int v = -NegaAlpha<false, end_ab>(rdepth - 1, -beta, -alpha, ppv2);
        if (v > alpha) {
            alpha = v;
            if (!root && alpha >= beta) {    // βカット;ただし根ノードではβカットは有り得ない
                node.Reset();
                return alpha;
            }
            pv_free(ppv);
            ppv = pv_alloc(curMove);
            ppv = pv_join(ppv, ppv2);
            if (root && alpha == INFINITE) { // 根ノードでINFINITEならもうおk やめてくれもう十分だ
                node.Reset();
                return alpha;
            }
        } else {
            pv_free(ppv2);
        }
    }
    node.Reset();

    return alpha;
}

int Search(int sdepth, int end_ab, TPVElem* ppv)
{
    return (end_ab != 0) ? NegaAlpha<true, 1>(sdepth, -INFINITE, INFINITE, ppv)
                         : NegaAlpha<true, 0>(sdepth, -INFINITE, INFINITE, ppv);
}

■ 結論

αβ探索はチョームズイマジヤバイ;

*1:MTD(f)? 何にそれおいしいの?

*2:根ノードだけα更新onlyの専用ルーチンにすれば良いというオチではある。つか、半年前のコードはPVを記録しないかわりにそうなっていた…

*3:相手を煙に巻ける