國際象棋

-
120 121 122 123 124 125 126 127 96 97 98 99 100 101 102 103 80 81 82 83 84 85 86 87 64 65 66 67 68 69 70 71 48 49 50 51 52 53 54 55 32 33 34 35 36 37 38 39 16 17 18 19 20 21 22 23 0 1 2 3 4 5 6 7 -
- 這樣,格子i的左邊一格是i - 1,右邊是i + 1,上邊是i + 16,下邊是i - 16,等等。棋盤被表示為128個格子的數(shù)組(其中64格代表棋盤上存在的格子),這種表示的優(yōu)勢在于:(1) 數(shù)組僅用1個指標(biāo),這樣寫的程序要比用2個指標(biāo)快;(2) 可以快速簡單地判斷某個格子i是否在棋盤上——當(dāng)且僅當(dāng)(i & 0x88) == 0時i在棋盤上。(因?yàn)榱谐龇秶鷷ri & 0x08不為零,而行超出范圍時i & 0x80不為零。)這是一個非常有效而且常用的技巧。
- 四、位棋盤
- 我必須在這里多花些筆墨,因?yàn)檫@個技術(shù)還不被人所熟悉,但是我認(rèn)為它可能會很好用的。以前用一個格子數(shù)組,每個元素含有一個棋子,現(xiàn)在取而代之的是一個棋子數(shù)組,每個元素代表棋盤上哪幾個格子有這個棋子,按位壓縮表示。由于棋盤上有64個格子,所以棋盤可以壓縮成一個64位的字(即兩個32位的字)。這種表示最大的優(yōu)勢在于,可以通過布爾操作(位操作)來加快局面評估和著法生成操作的速度。試想一下,如此煩瑣的事情可以用壓縮的字運(yùn)算來解決,例如下面的局面:
- 白方的兵(這個64位數(shù)字記作wP)應(yīng)該由這些位構(gòu)成:
- 這樣,格子i的左邊一格是i - 1,右邊是i + 1,上邊是i + 16,下邊是i - 16,等等。棋盤被表示為128個格子的數(shù)組(其中64格代表棋盤上存在的格子),這種表示的優(yōu)勢在于:(1) 數(shù)組僅用1個指標(biāo),這樣寫的程序要比用2個指標(biāo)快;(2) 可以快速簡單地判斷某個格子i是否在棋盤上——當(dāng)且僅當(dāng)(i & 0x88) == 0時i在棋盤上。(因?yàn)榱谐龇秶鷷ri & 0x08不為零,而行超出范圍時i & 0x80不為零。)這是一個非常有效而且常用的技巧。
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 1 0 0
- 0 0 0 0 0 1 0 0
- 0 0 0 0 1 0 0 0
- 0 0 0 0 0 0 0 0
- 1 1 1 0 0 0 0 1
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
-
- 而被黑方占據(jù)的格子可以用下面的公式來計(jì)算:
- 而被黑方占據(jù)的格子可以用下面的公式來計(jì)算:
- bOcc = bP | bN | bB | bR | bQ | bK
-
- (這里bP等等代表黑方不同兵種的棋子),類似地可以計(jì)算白方占據(jù)的格子,還可以把這兩個格子作“或”運(yùn)算得到所有被占的格子。這些白兵前進(jìn)一格可能到達(dá)的格子,可以用下面的公式計(jì)算:
- (這里bP等等代表黑方不同兵種的棋子),類似地可以計(jì)算白方占據(jù)的格子,還可以把這兩個格子作“或”運(yùn)算得到所有被占的格子。這些白兵前進(jìn)一格可能到達(dá)的格子,可以用下面的公式計(jì)算:
- single_pawn_moves = (wP << 8) & ~occupied
-
- 現(xiàn)在仔細(xì)看一下過程,先將wP左移8位,來產(chǎn)生每個兵前面的格子:
- 現(xiàn)在仔細(xì)看一下過程,先將wP左移8位,來產(chǎn)生每個兵前面的格子:
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 1 0 0
- 0 0 0 0 0 1 0 0
- 0 0 0 0 1 0 0 0
- 0 0 0 0 0 0 0 0
- 1 1 1 0 0 0 0 1
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 1 0 0
-
- 然后求被占格子的“非”運(yùn)算得到空的格子:
- 然后求被占格子的“非”運(yùn)算得到空的格子:
- 0 0 1 1 0 0 1 0
- 1 0 1 0 1 0 0 0
- 1 1 1 0 0 0 1 1
- 1 0 1 1 1 0 1 1
- 1 0 1 1 0 1 1 1
- 1 0 1 1 1 0 1 1
- 0 0 0 1 1 1 1 0
- 0 1 0 1 0 0 1 0
- 1 0 1 0 1 0 0 0
-
- 對以上兩個位棋盤按位作“與”運(yùn)算,就得到這些兵前面的沒有被占的格子了:
- 對以上兩個位棋盤按位作“與”運(yùn)算,就得到這些兵前面的沒有被占的格子了:
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 1 0 0 0
- 0 0 0 0 0 0 0 0
- 1 0 1 0 0 0 0 1
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
-
- 參照兵走一格的方法,可以找到兵走兩格的著法,即再左移8位,“與”上沒有子的格子,再“與”上一個只有第四行都占滿的常數(shù)棋盤(這是兵走兩格能到達(dá)的唯一一行):
- 參照兵走一格的方法,可以找到兵走兩格的著法,即再左移8位,“與”上沒有子的格子,再“與”上一個只有第四行都占滿的常數(shù)棋盤(這是兵走兩格能到達(dá)的唯一一行):
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 1 1 1 1 1 1 1 1
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0
-
- 值得注意的是,這個常數(shù)棋盤是在編譯的時候生成的,而不需要在每次著法生成的時候再計(jì)算出來。兵吃子的情況也類似,先左移7位或9位,再“與”上一個常數(shù)棋盤以過濾左邊線或右邊線的格子【對于左移7位產(chǎn)生吃右邊子時,需要考慮子在右邊線的情況,此時左移7位是同一行的左邊線,因此需要排除這個格子,即“與”上一個常數(shù)棋盤,代表除左邊線以外的所有格子】,最后“與”上bOcc【前面提到過這個棋盤,代表黑子所占的格子】。
- 位棋盤這個技術(shù)不能簡化代碼,但是它能一次就產(chǎn)生兵的所有著法,而不是一次只產(chǎn)生一個著法。此外,這個過程中有些表達(dá)式需要多次反復(fù)使用(例如bOcc),但只要計(jì)算一次就可以了。因此,位棋盤最終變得非常高效,在棋子數(shù)比國際象棋少的棋類中,我想位棋盤的效果會更好。
- 有個復(fù)雜的問題產(chǎn)生了:計(jì)算位棋盤中有多少非零位,或者找到【最低或最高的】一個非零位(例如把兵可以到達(dá)的位棋盤轉(zhuǎn)化為一系列著法),這些操作往往非常重要。計(jì)算位數(shù)的操作可以一次計(jì)算一個字節(jié),做一個長度為256的數(shù)組,每個元素代表它的指標(biāo)所含有多少個非零位,然后通過查表來完成。在找到一個非零位的計(jì)算中有個技巧:x ^ (x - 1)(“^”代表異或),會得到一個二進(jìn)制為...000111...的數(shù),x ^ (x - 1)的第一位就是x的最后一位非零位。如果要求得這個數(shù)字【x ^ (x - 1),即型如...000111...的數(shù)】的第一位,就把它對某個特定的數(shù)M取余數(shù)(不同的...000111...這樣的數(shù)對M取余數(shù)必須不同),然后去查表。例如,以下的代碼可以找到一個字節(jié)的最后一個非零位:
- 值得注意的是,這個常數(shù)棋盤是在編譯的時候生成的,而不需要在每次著法生成的時候再計(jì)算出來。兵吃子的情況也類似,先左移7位或9位,再“與”上一個常數(shù)棋盤以過濾左邊線或右邊線的格子【對于左移7位產(chǎn)生吃右邊子時,需要考慮子在右邊線的情況,此時左移7位是同一行的左邊線,因此需要排除這個格子,即“與”上一個常數(shù)棋盤,代表除左邊線以外的所有格子】,最后“與”上bOcc【前面提到過這個棋盤,代表黑子所占的格子】。
-
- int T = { -1, 0, 7, 1, 3, -1, 6, 2, 5, 4, -1, -1 };
- int last_bit(unsigned char b) {
- return T[(b^(b-1)) % 11];
- }
- int T = { -1, 0, 7, 1, 3, -1, 6, 2, 5, 4, -1, -1 };
-
- 【把原作者提到的這個技巧擴(kuò)展到16位或32位的情況,不妨可以試探一下(只要找得到合適的除數(shù)即可)。但是原作者沒有充分考慮計(jì)算機(jī)的運(yùn)算特點(diǎn),因此譯者認(rèn)為這不是一個合理的設(shè)計(jì)。由于“電腦一做除法就成了傻瓜”,所以代碼中取余數(shù)的過程嚴(yán)重影響了效率,為此譯者收集了一些使用炫技的程序,可以迅速完成求位數(shù)和查找位的操作。
- (1) 求一個32位數(shù)中有幾位非零位的運(yùn)算——Count32操作:
- 【把原作者提到的這個技巧擴(kuò)展到16位或32位的情況,不妨可以試探一下(只要找得到合適的除數(shù)即可)。但是原作者沒有充分考慮計(jì)算機(jī)的運(yùn)算特點(diǎn),因此譯者認(rèn)為這不是一個合理的設(shè)計(jì)。由于“電腦一做除法就成了傻瓜”,所以代碼中取余數(shù)的過程嚴(yán)重影響了效率,為此譯者收集了一些使用炫技的程序,可以迅速完成求位數(shù)和查找位的操作。
-
- int Count32(unsigned long Arg) {
- Arg = ((Arg >> 1) & 0x55555555) + (Arg & 0x55555555);
- Arg = ((Arg >> 2) & 0x33333333) + (Arg & 0x33333333);
- Arg = ((Arg >> 4) & 0x0f0f0f0f) + (Arg & 0x0f0f0f0f);
- Arg = ((Arg >> 8) & 0x00ff00ff) + (Arg & 0x00ff00ff);
- return (Arg >> 16) + (Arg & 0x0000ffff);
- }
- int Count32(unsigned long Arg) {
-
- (2) 求最低位非零位是第幾位的運(yùn)算——Lsb32操作(LSB = Least Significant Bit):
-
- int Lsb32(unsigned long Arg) {
- int RetVal = 31;
- if (Arg & 0x0000ffff) { RetVal -= 16; Arg &= 0x0000ffff; }
- if (Arg & 0x00ff00ff) { RetVal -= 8; Arg &= 0x00ff00ff; }
- if (Arg & 0x0f0f0f0f) { RetVal -= 4; Arg &= 0x0f0f0f0f; }
- if (Arg & 0x33333333) { RetVal -= 2; Arg &= 0x33333333; }
- if (Arg & 0x55555555) RetVal -=1;
- return RetVal;
- }
- int Lsb32(unsigned long Arg) {
-
- (3) 求最高位非零位是第幾位的運(yùn)算——Msb32操作(MSB = Most Significant Bit):
-
- int Msb32(unsigned long Arg) {
- int RetVal = 0;
- if (Arg & 0xffff0000) { RetVal += 16; Arg &= 0xffff0000; }
- if (Arg & 0xff00ff00) { RetVal += 8; Arg &= 0xff00ff00; }
- if (Arg & 0xf0f0f0f0) { RetVal += 4; Arg &= 0xf0f0f0f0; }
- if (Arg & 0xcccccccc) { RetVal += 2; Arg &= 0xcccccccc; }
- if (Arg & 0xaaaaaaaa) RetVal += 1;
- return RetVal;
- }
- int Msb32(unsigned long Arg) {
-
- 對64位數(shù)字進(jìn)行操作,把它分解成兩個32位字,分別對兩個字調(diào)用上面的函數(shù)就可以了。如果程序能運(yùn)行在64位的處理器上,只要對上面三個函數(shù)略加改動就可以了。】
- 如何撤消著法?
- 還記得嗎?我們說過在棋盤表示方法中需要涉及撤消著法的操作。現(xiàn)在有兩種解決方案:(1) 用一個堆棧來保存棋盤,執(zhí)行一個著法前先將棋盤壓入堆棧,撤消著法就從堆棧彈出棋盤,恐怕這太慢了…… (2) 用一個堆棧來保存著法,執(zhí)行一個著法前先將該著法及其相關(guān)信息壓入堆棧,撤消著法就根據(jù)該著法還原棋盤及其相關(guān)信息。例如,在國際象棋中只要保存被吃的棋子(如果吃子的話),以及王車易位和吃過路兵的權(quán)利等信息。
- 重復(fù)檢測
- 某些棋類在發(fā)生重復(fù)局面時要用到特殊的規(guī)則,如圍棋和國際象棋(在國際象棋中,第三次出現(xiàn)重復(fù)局面時,制造重復(fù)局面的一方就有權(quán)宣布和棋)。那么如何知道是否出現(xiàn)重復(fù)局面呢?答案很簡單:用散列函數(shù)把局面轉(zhuǎn)換成一個相當(dāng)大的數(shù)(我們以后要談到這個問題,因?yàn)檫@個技術(shù)還可以加快搜索速度),然后保存一系列前面出現(xiàn)過的局面的散列值,從這些值里面找就是了。一個典型的散列函數(shù),先隨機(jī)產(chǎn)生一張64x13的表,如果棋子y在位置x上,就把表中[x, y]這個數(shù)加到散列值上(忽略溢出)[即Zobrist值]。值得注意的是,當(dāng)棋子y從位置x走到位置z時,可以快速地更新散列值:減去[x, y]并加上[z, y]即可。
- 對64位數(shù)字進(jìn)行操作,把它分解成兩個32位字,分別對兩個字調(diào)用上面的函數(shù)就可以了。如果程序能運(yùn)行在64位的處理器上,只要對上面三個函數(shù)略加改動就可以了。】
posted on 2007-08-10 17:21 小鋒 閱讀(676) 評論(0) 編輯 收藏 所屬分類: algorithm