• 沒有找到結果。

Transposition Table

在文檔中 電腦暗棋之設計及實作 (頁 34-42)

第三章 搜尋演算法

第七節 Transposition Table

在對局樹的搜尋過程中,不少盤面節點雖然是經過不同走法順序所到達的,

但其盤面狀態有可能是完全一致的,如圖3-14:

圖3-14 相同盤面示意圖

不管是走路徑:E2-F2,E4-F4,F2-F3,G3-H3,還是走路徑:E2-E3,E4-F4,E3-F3,

圖3-15 相同盤面示意圖

從以上的例子可以知道,如果之前有某個盤面節點已經搜尋過的話,當再次

遇到同樣狀態盤面節點時,就可以套用之前搜尋的結果,省下再重複搜尋的時間。

而Transposition Table(又稱 Hash Table),則是一個可以將已搜尋過的節點資

訊記錄起來的雜湊表。節點資訊通常包括:盤面key 值、節點搜尋深度、節點分

數、節點類型…等。一般使用Zobrist Hash 的方式來進行盤面 key 值的生成,以

達到快速檢測當前節點是否已經搜尋過的目的。

Zobrish Hash 方法是在搜尋之前,預先產生大隨機數的二維陣列 Zobrist[棋子

類型][棋子位置],至於大隨機數要多大?在我們這次的暗棋程式實作裡是使用

32 位元的無號整數,有了 Zobrist[棋子類型][棋子位置] 這樣的二維陣列以後,

當前盤面的key 值,便是所有棋盤上的棋子相對應到 Zobrist[棋子類型][棋子位置]

二維陣列做xor 運算(以^表示)後的和。這樣在有著法產生移動時,並不需要重

新計算盤面的key 值,只需要將當前 key 值(假設叫:ZobristKey)做以下步驟[8]:

1. ZobristKey^=Zobrist[移動棋子類型][移動棋子原位置]。

2. ZobristKey^=Zobrist[移動棋子類型][移動棋子新位置]。

3. 如果著法是吃子著法的話,需要再做:ZobristKey^=Zobrist[被吃棋子類型][被

吃棋子位置]。

而為了避免盤面相同輪走方不同卻擁有一樣的key 值的情況,我們還需要一個大

隨機數 ZobristPlayer 來代表輪走方,每走一步就做 ZobristKey^=ZobristPlayer 運

算,最後的ZobristKey 才是真正代表盤面且加上輪走方訊息的 key 值。如果不加

ZobristKey^=ZobristPlayer 運算會怎樣呢?我們以如圖 3-16 輪紅方走當做例子,

此時的盤面key 值 ZobristKey = Zobrist[卒][0] ^ Zobrist[兵][1] ^ Zobrist[炮][2] ^

Zobrist[ 卒 ][7] = 816894721 ^ 138765628 ^ 3996203608 ^ 1478426211 =

1812624575。

圖3-16 盤面相同輪走方不同示意圖

紅方走了C1-A1 吃卒後,如圖 3-17,此時輪黑方走,此時的盤面 key 值 ZobristKey

= ZobristKey ^ Zobrist[炮][2] ^ Zobrist[炮][0] ^ Zobrist[卒][0] = 1812624575 ^

3996203608 ^ 3032318783 ^ 816894721 = 2935939268。

圖3-17 盤面相同輪走方不同示意圖

如果圖3-16 紅、黑雙方依路徑:B1-A1 吃兵,H1-H2,C1-C2,H2-H1,C2-B2,

H1-H2,B2-A2,H2-H1,A1-B1,H1-H2,A2-A1,H2-H1,也可以到達如圖 3-17

的盤面,但此時是輪紅走,而盤面key 值仍舊是 2935939268。所以如果只用棋盤

上所有棋子相對應的 Zobris t[棋子類型][棋子位置]做 xor 後的總和,這時就會發

生盤面相同但輪走方不同卻擁有相同 key 值的情況,而造成搜尋時節點資訊的誤

用。如果有在每走一步做ZobristKey^=ZobristPlayer 運算,以圖 3-16 為例,輪紅

方走時 ZobristPlayer 必然跟 ZobristKey 做了偶數次 xor 運算,而輪黑方走時

ZobristPlayer 必然跟 ZobristKey 做了奇數次 xor 運算,所以即使原來的 ZobristKey

相同,也會因為跟 ZobristPlayer 做了奇、偶次數 xor 運算的關係,而讓最後的

ZobristKey 值不同。

Hash Table 是一種空間換取時間的思維,由於實際記憶體空間有限,所以我

們不可能替Hash Table 設置一個無限大的空間供其使用,因此我們必須對所產生

的節點是否存入Hash Table 做些限制,避免一些無用的節點資訊佔據了 Hash Table

的空間,而這些無用的節點通常是一些距離根節點很遠的節點,因為它們被重複

搜尋的機率很低。還有當兩個節點盤面值都對應到同一個slot 位置時,也必須有

因應的策略。其中slot 的算法為:slot = 盤面 key 值%Hash_Table_Size。其中%表

示求餘數的運算。

在我們這次暗棋程式的實作中,Hash_Table_Size 為 1M,剛好是 220,因為在

計算slot 位置時必須用到除法指令,但電腦一做除法就會很耗時,所以如果能將

Hash_Table_Size 設為 2N這個常量,想要得到除以2N 後的餘數,只要做如下修改:

slot = 盤面 key 值& ( Hash_Table_Size – 1 ),以圖 3-17 輪紅走為例,可算出 slot =

盤面key 值 2935939268 &(1M – 1)=2935939268 & 1048575 = 975044,而電腦

做&運算(AND)的速度是非常快的。

還有我們對於寧靜搜尋的節點不存入Hash Table,避免過多距離根節點很遠

的節點充斥整個Hash Table ,而它們被再利用的機率卻很小。而對於盤面值對應

到同一個slot 位置時,我們採用“深度優先覆蓋"的策略,所謂的“深度優先覆

蓋"即待存入Hash Table 的節點的深度必須大於或等於已存入 Hash Table 節點的

深度才覆蓋。據研究表示:深度優先覆蓋的效果要比始終覆蓋或完全不覆蓋要好

得多[2],且程式撰寫起來並不複雜。如圖 3-18 是節點資訊存入 Hash Table 的函

式虛擬碼:

圖3-18 節點存入 Hash Table 的函式虛擬碼

從上面的虛擬碼可以看出,Hash Table 裡最後還存了節點類型。而為什麼已

經存入了節點分數,還要再存入節點類型?這是因為在Alpha-Beta 搜尋演算法的

過程中,任何節點的分數都是下列三種情況之一:

(一)節點分數 ≧ beta,即所謂的 beta 節點。

(二)節點分數 ≦ alpha,即所謂的 alpha 節點。

(三)alpha < 節點分數 < beta,即所謂的 PV(Principal Variation)節點。

一般來說,只有PV 節點的分數,才可以當作是節點的準確值存入 Hash Table

,其餘的 beta 節點、alpha 節點的分數只能表示是節點分數的一個邊界而已。但 void RecordHash(int depth,int val,int hashf){

HashStruct *p = &Hash_Table[ZobristKey & (Hash_Table_Size-1)];

if(depth>=p->depth){ //深度優先覆蓋,存入節點相關資訊

存入這樣的邊界分數,仍有助於我們下次搜尋到同樣盤面節點的時候,進行剪裁 int ProbeHash(int depth,int alpha,int beta){

HashStruct *p = &Hash_Table[ZobristKey & (Hash_Table_Size-1)];

if(ZobristKey==p->key&&p->depth>=depth){ //探測成功 if(p->hashf==PV_FLAG){

return p->val;

}

if(p->hashf==BETA_FLAG&&p->val>=beta){ //與上界 beta 比較 return beta;

}

if(p->hashf==ALPHA_FLAG&&p->val<=alpha){//與下界 alpha 比較 return alpha;

} }

return valUNKNOWN; //探測失敗或是剪裁失敗 }

前節點的深度。這是因為Hash Table 中的節點深度如果比當前節點深度淺的話,

所存的節點資訊還不足以讓當前節點拿來使用,因為它的搜尋深度還不夠深。只

有在Hash Table 的節點深度大於或等於當前節點深度的時候,我們才能拿裡面的

資訊來利用。圖3-20 是 Alpha-Beta 搜尋結合 Hash Table 的虛擬碼:

圖3-20 Alpha-Beta 結合 Hash Table 的虛擬碼 int AlphaBeta(int depth,int alpha,int beta){

int val,foundPV=0;

if(depth==0) return Quies(alpha,beta); //進入寧靜搜尋 if((val==ProbeHash(depth,alpha,beta))!=valUNKNOWN){ //探測 Hash Table return val;

}

GenerateLegalMoves();

while(MovesLeft()){

MakeNextMove();

val= -AlphaBeta(depth-1,-beta,-alpha);

UnMakeMove();

if(val>=beta){

RecordHash(depth,beta,BETA_FLAG); //存入 beta 節點 return beta;

}

if(val>alpha){

foundPV=1; //有找到 PV 節點 alpha=val;

} }

if(foundPV) RecordHash(depth,alpha,PV_FLAG); //存入 PV 節點 else RecordHash(depth,alpha,ALPHA_FLAG); //存入 alpha 節點 return alpha;

}

在文檔中 電腦暗棋之設計及實作 (頁 34-42)

相關文件