關於AlphaBeta剪枝的文章太多,這個方法是所有其它搜索方法的基礎,得多花些時間認真地理解。
先把基本概念再回顧一遍:
節點:在中國象棋中就是一個棋盤的當前局面Board,當然該輪到誰走棋也是確定的。這里的圓形節點表示終止節點,在中國象棋里就是一方被將死的情況(或者到達了搜索的最大深度),后續不會再有着法產生,游戲如果走到這里就會結束。在引擎里通常給紅方一個很大的評估值,如+30000,給黑方一個很小的評估值,如-30000,來方便地判斷這種結束局面。(勝利局面有稍微不同的表示法,用-30000+層數ply來表示)
連線:表示一步着法Move,通過這步着法后,局面發生變化,先后手也要交換。
層:通常的術語是ply,復數形式是plies,也有稱為levels,當然與depth也是對應的。這個術語是為了與比賽里常說的回合相區分,一個回合通常包含2步,這個ply就表示某一方走了一步。根節點記為0層,以下的層數遞增。
深度depth:要注意是從下到上的,還是從上到下的。(1)通常的算法書中都是從下到上遞增的,即根節點為最大搜索深度,走到最底部的葉子結點為0,這種算法只要記住一個depth值就可以了。(2)而另一種記法是根部結點為0,越向下depth增加,這時在搜索時就要傳遞2個參數值,depth和maxDepth,稍微有點啰嗦,應該也會影響一點效率。另外在探查置換表中的結點時,用第(1)種記法也方便一些,因為要知道從當前節點迭代的深度值,否則還要在置換表中保存depth和maxDepth兩個值。
AlphaBeta剪枝方法是對Minimax方法的優化,它們產生的結果是完全相同的,只不過運行效率不一樣。
這種方法的前提假設與Minimax也是一樣的:
1)雙方都按自己認為的最佳着法行棋。
2)對給定的盤面用一個分值來評估,這個評估值永遠是從一方(搜索程序)來評價的,紅方有利時給一個正數,黑方有利時給一個負數。(如果紅方有利時返回正數,當輪到黑方走棋時,評估值又轉換到黑方的觀點,如果認為黑方有利,也返回正數,這種評估方法都不適合於常規的算法描述)
3)從我們的搜索程序(通常把它稱為Max)看來,分值大的數表示對己方有利,而對於對方Min來說,它會選擇分值小的着法。
但要注意:用Negamax風格來描述的AlphaBeta中的評估函數,對輪到誰走棋是敏感的。
也就是說:
在Minimax風格的AlphaBeta算法中,輪紅方走棋時,評估值為100,輪黑方走棋評估值仍是100。
但在Negamax風格的AlphaBeta算法中,輪紅方走棋時,評估值為100,輪黑方走棋時評估值要為-100。
貼一段偽代碼:
def ABNegaMax (board, depth, maxDepth, alpha, beta) if ( board.isGameOver() or depth == maxDepth ) return board.evaluate(), null bestMove = null bestScore = -INFINITY for move in board.getMoves() newBoard = board.makeMove(move) score = ABNegaMax(newBoard, maxDepth, depth+1, -beta, -max(alpha, bestScore)) score = -score if ( score > bestScore ) bestScore = score bestMove = move # early loop exit (pruning) if ( bestScore >= beta ) return bestScore, bestMove return bestScore, bestMove
用下列語句開始調用:
ABNegaMax(board, player, maxDepth, 0, -INFINITY, INFINITY)
// method call with depth 5 and minimum and maximum boundaries // minimaxValue = alphaBeta(board, 5, -MATE, +MATE) int alphaBeta(ChessBoard board, int depth, int alpha, int beta) { int value; if( depth == 0 || board.isEnded()) { value = evaluate(board); return value; } board.getOrderedMoves(); int best = -MATE-1; int move;
ChessBoard nextBoard; while (board.hasMoreMoves()) { move = board.getNextMove(); nextBoard = board.makeMove(move); value = -alphaBeta(nextBoard, depth-1,-beta,-alpha); if(value > best) best = value; if(best > alpha) alpha = best; if(best >= beta) break; } return best; }
下面這個PDF更清楚地說明了Negamax風格的alphabeta算法的過程:
http://www.cs.colostate.edu/~anderson/cs440/index.html/lib/exe/fetch.php?media=notes:negamax2.pdf
為了更准確地理解minmax和negamax兩種不同風格的搜索執行過程,畫出了詳細的圖解,發現negamax更容易理解了,代碼也確實精練了不少。
當采用了置換表算法后,還需要對PV-Nodes, Cut-Nodes和All-Nodes三種類型結點的含義以及如何變化有更准確地了解。
可以發現,Negamax風格的算法有下面的特點(代碼來自象棋巫師):
int AlphaBeta(int depth, int alpha, int beta) {
int hashf = hashfALPHA; //開始時的結點類型應該是All-Nodes,有些地方稱為ALPHA類型結點
//這里要探查置換表
if (depth == 0) { // 葉子結點,評估,寫入置換表,返回即可
val = Evaluate();
RecordHash(depth, val, hashfEXACT); // 葉子結點肯定是PV-Nodes
return val;
}
GenerateLegalMoves();
while (MovesLeft()) {
MakeNextMove();
//注意Negamax風格的調用方式,前面有個負號,后面的參數是-beta和-alpha
// Negamax的含義中Nega就是指這里的負號
val = -AlphaBeta(depth - 1, -beta, -alpha);
UnmakeMove();
if (val >= beta) { // 剪枝情況判斷
RecordHash(depth, beta, hashfBETA); //這時的結點類型是Cut-Nodes
return beta;
}
if (val > alpha) { // Negamax中的max就是指的這一段,要記錄最大的評估值,這里沒有引入一個新變量,直接就用了alpha變量
hashf = hashfEXACT; // 只要alpha值一發生變化,這個結點類型就是PV-Nodes了!
alpha = val;
}
}
RecordHash(depth, alpha, hashf);
return alpha; // 此時的alpha就是記錄了當前結點的所有子結點的最大的負評估值!
}
這面的代碼在置換表探查方面感覺有點問題,又從Marek Strejczek的論文《Some aspects of chess programming》的第87頁上找到一段代碼,感覺這段代碼充分利用了置換表中存儲的信息。
chSCORE alphabetaWithTT(chPOSITION node,chSCORE alpha,beta) { if (isLeaf(node) ) return evaluate(node); if ( (entry = getFromHash(node) ) != NULL) { if (TT_entry_deep_enough) { // data in hash table comes from a search to the // same depth as current or deeper – so it is reliable if (entry.flag == UPPER) { if (entry.score <= alpha) { return alpha } if (entry.score < beta) beta = flag.score; } } if (entry.flag == LOWER) { if (entry.score >= beta) { return beta; } if (entry.score > alpha) { alpha = flag.score; } } if (entry.flag == EXACT) { return entry.score } } else { // TT entry represents results of a shallower // depth search – no cutoffs possible, but still // a valuable move to try first can be used if (entry.move != NULL) { try_hash_move_first = true; } } } g = alpha; x = left_most_child(node); hash_flag = UPPER; best_move = NULL; while (isNotNull (x) ) { g = -alphabeta(x, -beta, -alpha); if (g > alpha) { alpha = g; if (g >= beta) { hash_flag = LOWER; best_move = current_move; saveHash(node,g,hash_flag,best_move); return beta; } hash_flag = EXACT; best_move = current_move; } x = next_brother(x); } putToHash(node, g, hash_flag, best_move) return alpha; } // end of alphabetaWithTT