【Python 電脳闘技場編 #6】無駄な未来を切り捨てろ!「Alpha-Beta(アルファ・ベータ)枝刈り」の魔術
第0章:冷酷なる光の刃(ブレード)
電脳闘技場の闇の中、Stoneの小さなハサミの先に、まばゆいシアンの光刃(ブレード)が形成されていく。
彼の目の前には、Minimax関数が生成した数百万という「未来の分岐(ゲーム木)」が、複雑なフラクタル構造を描いて浮かび上がっていた。

美しく、そして無駄が多い。Minimaxはすべての未来を完璧に計算するが、ハッカーから言わせれば『バカ正直』すぎるんだ。どう考えても選ばない最悪な未来を、わざわざ最後まで計算している暇なんてない。
Stoneが光刃を一閃すると、計算する価値のない未来の枝(ノード)が次々と切り落とされ、闇へと消えていく。
50msという極限の時間内で探索深度(深さ)を劇的に引き上げる、計算機科学の魔術。
「枝刈り(Pruning)」の時間が始まった!
第1章:なぜ「枝刈り」が必要なのか?
前回構築したMinimax関数は、すべての手に対して指定した深さまで探索を行う。
しかし、ゲーム木が O(bd) で爆発する中、「明らかに不利な手」のその先を深く調べるのは計算資源の無駄だ。
例えば、ある分岐で「自分のクイーンがタダで取られる」ことが確定したとしよう。その手を打つと圧倒的に不利になることがわかっているなら、その先で「ポーンが取れるか、ナイトが取れるか」なんて細かな計算をする必要はない。「この手はダメだ」と見切って、すぐに次の手の計算に移るべきだ。
これをアルゴリズムで実現するのが「Alpha-Beta(アルファ・ベータ)枝刈り」である。
第2章:AlphaとBetaの論理
この魔術を実装するためには、前回の関数に alpha と beta という2つの変数を追加し、未来へ伝播させていく必要がある。
- Alpha(アルファ): 自分のターン(Maximizer)において、「最低でもこれ以上のスコアは確保できる」という保証ライン。(初期値は -∞)
- Beta(ベータ): 敵のターン(Minimizer)において、「敵が許す最高のスコア(これ以上スコアが高い手は、敵が絶対に選ばせない)」という上限ライン。(初期値は +∞)

探索を進める中で、もし 「Beta <= Alpha」 という状態になったら、それは「敵が絶対にこのルートを選ばない」または「自分がもっと良い別のルートを持っている」ことを意味する。だから、その先の未来はスパッと『枝刈り(探索を打ち切り)』するんだ!
第3章:Alpha-Betaエンジンの実装
それでは、前回の minimax 関数を alpha_beta 関数へと進化させよう。
型ヒント(Type Hints)を追加し、より堅牢なハッカーのコードに仕上げていくぜ。
# ※ Stateクラス、evaluate_board関数が定義済みであることを前提とする
def alpha_beta(state: 'State’, depth: int, alpha: float, beta: float, is_maximizing_player: bool) -> float:
# 探索の限界に達したか、勝敗が決まった場合
if depth == 0 or state.is_game_over():
return evaluate_board(state)
if is_maximizing_player:
max_eval: float = -float('inf')
for move in state.get_legal_moves():
next_state: 'State' = state.clone()
next_state.apply(move)
# 再帰呼び出し(敵のターンへ)
eval_score: float = alpha_beta(next_state, depth - 1, alpha, beta, False)
max_eval = max(max_eval, eval_score)
# Alpha値(自分が確保した最低ライン)を更新
alpha = max(alpha, eval_score)
# 枝刈り(Beta <= Alpha なら、これ以上探索しない)
if beta <= alpha:
break # ループを抜けて未来を切り捨てる!
return max_eval
else:
min_eval: float = float('inf')
for move in state.get_legal_moves():
next_state: 'State' = state.clone()
next_state.apply(move)
# 再帰呼び出し(自分のターンへ)
eval_score: float = alpha_beta(next_state, depth - 1, alpha, beta, True)
min_eval = min(min_eval, eval_score)
# Beta値(敵が許す最高ライン)を更新
beta = min(beta, eval_score)
# 枝刈り
if beta <= alpha:
break # 敵にとって不利すぎるルートなので切り捨てる!
return min_eval
# ※ Stateクラス、evaluate_board関数が定義済みであることを前提とする
def alpha_beta(state: 'State', depth: int, alpha: float, beta: float, is_maximizing_player: bool) -> float:
# 探索の限界に達したか、勝敗が決まった場合
if depth == 0 or state.is_game_over():
return evaluate_board(state)
if is_maximizing_player:
max_eval: float = -float('inf')
for move in state.get_legal_moves():
next_state: 'State' = state.clone()
next_state.apply(move)
# 再帰呼び出し(敵のターンへ)
eval_score: float = alpha_beta(next_state, depth - 1, alpha, beta, False)
max_eval = max(max_eval, eval_score)
# Alpha値(自分が確保した最低ライン)を更新
alpha = max(alpha, eval_score)
# 枝刈り(Beta <= Alpha なら、これ以上探索しない)
if beta <= alpha:
break # ループを抜けて未来を切り捨てる!
return max_eval
else:
min_eval: float = float('inf')
for move in state.get_legal_moves():
next_state: 'State' = state.clone()
next_state.apply(move)
# 再帰呼び出し(自分のターンへ)
eval_score: float = alpha_beta(next_state, depth - 1, alpha, beta, True)
min_eval = min(min_eval, eval_score)
# Beta値(敵が許す最高ライン)を更新
beta = min(beta, eval_score)
# 枝刈り
if beta <= alpha:
break # 敵にとって不利すぎるルートなので切り捨てる!
return min_eval

たった数行の if beta <= alpha: break を追加しただけだ。しかし、この数行のコードが、探索スピードを数倍〜数十倍に跳ね上げる。同じ50msでも、Minimaxより数手先まで深く潜れるようになるんだ。
次回、第7回:「枝刈りの刃を研ぎ澄ませ!『Move Ordering(手番の並び替え)』」。
Alpha-Beta枝刈りは強力だが、実は「探索する順番」によってその切れ味が劇的に変わる。ハッカーならではの小技で、枝刈りの効率を極限まで引き上げる手法を解説するぜ!
🛠️ Stoneの愛用ギア(ハッカーの開発環境)

無駄を極限まで削ぎ落とすアルゴリズムを組むなら、自分の指先から生じる「入力の無駄(タイプミス)」も削ぎ落とすべきだ。僕が思考をダイレクトに電脳世界へ出力するためのデバイスがこれさ。(スポンサーリンク)
プログラマー至高のメカニカルキーボード。無駄なキーを削ぎ落とした洗練された配列と、静電容量無接点方式の極上の打鍵感。Alpha-Betaのように、ノイズ(無駄な疲労)を切り捨ててくれるハッカー必須のギアだ。







ディスカッション
コメント一覧
まだ、コメントがありません