第三章 搜尋演算法
第五節 Alpha-Beta 搜尋演算法
第五節 Alpha-Beta 搜尋演算法
在Min-Max 或是 Nega-Max 搜尋演算法中,對於對局樹裡的每個節點的每一
個所有可能著法都要走過一次,假設對局樹中每個節點平均有 30 個著法,如果
要搜尋N 層,就要拜訪大約 30N個節點,因此這樣做需要耗費相當多的時間,效
率非常地差[7]。
Brudno 在 1963 年提出了 Alpha-Beta 搜尋演算法,它在大部份的情況下會比
Min-Max 有更佳的搜尋效率。而 Alpha-Beta 的核心思想是:在搜尋的過程中,發
現無論如何都無法改變對方目前的最佳分數時,就可以提早放棄,不必浪費時間
搜尋其它的著法。例如圖3-6:
圖3-6 Alpha-Beta 示意圖
圖 3-6 中,方形節點會從子節點中取最大值,圓形節點會從子節點中取最小值,
而葉節點則會傳回以根節點為評分角度的審局函數值。其中節點A,經由深度優
先搜尋左子樹,得到左子節點的值為30,由於節點 A 會從其子節點中取最大值,
所以A 的值必不小於 30,換句話說,A 的右節點的值必須大於 30 ,才可能會被
A 選上。
當找尋過B 的左子節點時,得到分數 20,因為 B 要取最小值,所以 20 便是
B 的最大可能值。然而,B 之值必須大於 30 才會被 A 選上,且 B 剩下的子節點
無論分數如何,都不可能使B 的值大於 30,所以就不用往下搜尋剩下的節點了。
Alpha-Beta 搜尋演算法必須使用兩個參數,分別是 α 和 β。其中 α 是記錄最
大層節點目前的最大值,而β 是記錄最小層節點的最小值,兩個參數以傳值 call
by value 的方式傳遞給下層的子樹。如果在取最大值的時候,發現了一個大於等
於β 的值,就不用再對其它分枝進行搜尋,這就是所謂的 β 截斷;同理,在取最
小值的時候,發現了一個小於等於α 的值,也不用再對其它分枝進行搜尋,這就
是所謂的α 截斷。例如圖 3-7 是 Alpha-Beta 另一個比較複雜的範例:
圖3-7 Alpha-Beta 示意圖
圖3-7 是 Alpha-Beta 以深度優先從左至右拜訪的樹狀圖,其節點截斷順序如下:
1. B 節點取值 25 的時候,25 ≧ 20,造成 C 節點的截斷(β 截斷)。
2. E 節點取值 6 的時候,6 ≦ 20,造成 F 節點的截斷(α 截斷)。
3. H 節點取值 3 的時候,3 ≦ 6,造成 I 節點的截斷(α 截斷)。
4. J 節點取值 6 的時候,6 ≦ 20,造成 K 節點的截斷(α 截斷)。
很顯然地,Alpha-Beta 搜尋演算法的搜尋效率與走法的排列順序有極密切的
關係。如果總是先嘗試壞的走法的話,那最終將會與 Min-Max 搜尋演算法一樣,
完全沒有任何截斷的機會。倘若能將好的走法排在前面優先嘗試的話,就可以儘
早發生截斷,省掉對其它節點不必要的搜尋。例如圖3-7 中,總共有 31 個節點,
最終只搜尋了19 個節點,省了 12 個節點。在最佳走法搜尋順序時,Knuth 和 Moore
int AlphaBeta(int depth,int alpha,int beta){
int val;
if(depth==0){
return Evaluate();
}
GenerateLegalMoves();
while(MovesLeft()){
MakeNextMove();
val= -AlphaBeta(depth-1,-beta,-alpha); //alpha 和 beta 順序對調 //且加上負值
UnMakeMove();
if(val>=beta){
return beta; //beta 截斷 }
if(val>alpha){
alpha=val;
} }
return alpha;
}
以Nega-Max 形式寫成的 Alpha-Beta 搜尋演算法,當 α 和 β 兩個參數在傳遞
給下層的時候,是將兩個順序對調,並加上負值,這樣做使得α 截斷也能簡化成
以β 截斷的形式表現出來,消除了 Min-Max 形式的 Alpha-Beta 搜尋演算法,在 α
截斷與β 截斷判斷形式上的不同,並且讓程式碼更簡潔有力。在我們這次暗棋程
式的實作中,也是以Nega-Max 形式的 Alpha-Beta 搜尋演算法為其最主要核心。
第六節 寧靜搜尋
一般利用Alpha-Beta 搜尋演算法所展開的對局樹深度有限,所以在某一固定
深度之下的情形將會被忽略,這時就經常會有所謂的水平效應(Horizontal Effect)
的產生。例如圖3-9 輪黑方走搜尋 10 層:
圖3-9 水平效應示意圖
當搜尋路徑為:C1-D1,F2-F4 吃卒,D1-E1 吃炮,H3-H4 吃卒,E1-D1,H4-H3,
D1-C1,E4-E3,C3-C4,F4-C4 吃馬,總共 10 步後,到達如圖 3-10 的盤面:
圖3-10 水平效應示意圖
此時因為沒有剩餘深度可以讓黑方嘗試走B4-C4 吃炮,導致黑方最後得到吃炮+
18 分、損失兩隻卒-2 分、損失一隻馬-6 分,總共 10 分這樣誤判的結果。
為了解決上述的問題,我們使用寧靜搜尋(Quiescent Search)。當沒有剩餘
深度時不立刻返回審局函數的評估分數值,而是根據盤面只針對吃子步進行展開
,直到最後的寧靜盤面,也就是不再有棋子被吃為止,才返回審局函數的值。這
樣的做法多少可以克服因水平效應而造成的搜尋不準確的結果。
圖3-11 是 Quiescent Search 的虛擬碼:
圖3-11 Quiescent Search 虛擬碼
在我們撰寫 Quiescent Search 的過程中,有看過許多版本的虛擬碼,它們都
會在GenerateCapMoves()之前,先呼叫審局函數 Evaluate(),看審局函數的值是否
可以造成β 截斷,如果可以造成 β 截斷就直接返回 beta 值,不過我們發現這樣做
的結果可能會導致搜尋結果不正確。原因如圖 3-12,輪黑方走,進行 10 層的搜
尋:
int Quies(int alpha,int beta){
int val;
GenerateCapMoves(); //只產生吃子步 while(MovesLeft()){
MakeNextMove();
val= -Quies(-beta,-alpha); //遞迴呼叫時不再需要 depth 參數 UnMakeMove();
if(val>=beta){
return beta;
}
if(val>alpha){
alpha=val;
} }
return alpha;
}
圖3-12 寧靜搜尋示意圖
當搜尋路徑為:F1-G1,F2-G2,H3-H2,E1-F1,H2-G2 吃兵,F1-E1,B1-A1,
E1-F1,A1-B1,F1-G1 吃包,總共 10 步後,到達如圖 3-13 的盤面:
圖3-13 寧靜搜尋示意圖
此時輪黑方走,如果這時呼叫審局函數Evaluate(),會得到-17 分的值,而這時窗
口的beta 值是-18,-17 大於-18 會造成所謂的 β 截斷,所以將分數回傳上去,最
後導致圖3-12 輪黑方走的搜尋結果為-17 分。這樣的搜尋結果是錯的,因為正確
的結果應該是紅、黑雙方在10 層的搜尋內,互相都吃不到對方的子,返回 0 分
才對。所以,寧靜搜尋當盤面還有吃子步可以嘗試的時候,不應該直接用審局函
數的值來當做是否可以截斷的依據,而直接返回beta 值,以免造成搜尋結果的不
正確。