Feng.Li's Java See

          抓緊時間,大步向前。
          隨筆 - 95, 文章 - 4, 評論 - 58, 引用 - 0

          導航

          <2007年8月>
          2930311234
          567891011
          12131415161718
          19202122232425
          2627282930311
          2345678

          常用鏈接

          留言簿(7)

          隨筆分類

          隨筆檔案

          文章檔案

          相冊

          大家都在博

          • 東東同學
          • 也許,在每個人的心靈深處,都會有一份屬于自己的寧靜
          • 亞明先生
          • 誰說世間無高人?且看我“物質生活”
          • 大飛
          • 此大飛,非彼大飛,乃宿舍長兼學生會主席
          • 玉東同學
          • 小男人

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜

          國際象棋



          棋盤的表示
           
          David Eppstein */
          * 加州愛爾文大學(UC Irvine)信息與計算機科學系
           
            要讓機器下棋,程序首先必須對兩個對象進行存儲和操作,它們是局面和著法。這兩個對象的表示使得程序可以進行下列操作:
            (1) 執行一個著法(不僅僅是用戶所指出的著法,而是在搜索過程中要用到的)
            (2) 撤消一個著法(作用同上);
            (3) 向用戶顯示棋盤;
            (4) 產生所有可能的著法;
            (5) 對棋盤的局面作出評估。
            除了顯示棋盤以外,所有的操作都應該盡可能地迅速,因為它們會在搜索的循環內被調用。(而顯示棋盤的操作畢竟不是經常要做的。【譯注:在UCI協議(國際象棋通用引擎協議)中,引擎從不顯示棋盤,因此這個操作事實上是多余的?!?/font>)
            著法的內部表示應該是非常簡潔的(因為我們不需要花太多的時間來生成一系列著法)而且能快速解讀,但是非常重要的是,它應該能代表所有的著法!在國際象棋中,機器內典型的著法表示,就是記錄移動棋子的起點格子和終點格子,例如王前兵進兩格就表示為“e2e4(e2代表這個兵起點位置而e4代表終點位置)。不管是否吃子,被吃的棋子都不必記錄,因為它可以由終點格子來判斷。在機器中位置能表示為6位的數值,所以一個著法可以用2個字節表示。盡管很多程序都是這樣表示的,但是這種表示方法不足以表示所有的著法!王車易位時,有兩個子要移動,此時我們把它當作特殊情況來考慮,只記錄王的移動。問題在于,當兵從第七行走到第八行時,可以升變為后、車、馬或象,上述表示方法不能讓我們指定升變為哪個棋子。因此在設計著法表示時需要非常仔細,要確認這種表示能夠涵蓋棋局中所有可能發生的特殊情況。
            【一般而言,棋類的著法有兩種形式,即添子式和移動式。五子棋、圍棋、黑白棋等屬于添子式,記錄著法時只要記錄棋子所下到的那個格子。其中五子棋最簡單,下完的棋子不再會改變;黑白棋稍復雜些,下完的棋子可能會被后續著法所變換黑白,但每下一子棋盤上就多一子;圍棋是最復雜的,由于存在提子的著法,所以局勢是可逆的,打劫這樣的著法需要很復雜的處理過程。
            國際象棋和中國象棋都屬于移動式,相比較而言中國象棋更簡單,當一個棋子從起點移到終點時,只要簡單地做一下終點的覆蓋即可;而國際象棋由于三條特殊規則的存在而必須做特別處理,有時別的特殊位置的棋子要跟著移動(王車易位),有時別的特殊位置的子要被吃掉(吃過路兵),有時移動的棋子要被其他棋子所替代(升變)。】
            棋盤的表示可能稍許復雜了些,但也不應該太龐大。它必須包括所有相關的信息,而不僅僅是表觀上的,但無關的信息不包括在其中。例如在國際象棋里,我們必須知道棋子在棋盤上的位置(表觀上的信息),而且需要知道一些不可見的信息——現在是誰在走,每方是否還有易位權,哪個過路兵可以吃,從吃子或進兵到現在一共走了多少步。我們還需要知道以前發生過哪些重復的局面(因為三次重復局面即導致和棋),而不需要知道以前所有的著法。
            【在網絡通訊中常常用一種稱為FEN串的6段式代碼來記錄局面,在國際象棋中它的6段代碼依次為:棋盤、走子方、王車易位權、吃過路兵的目標格、可逆著法數以及總回合數,基本上涵蓋了國際象棋某個局面的所有信息。但是FEN字符串無法記錄重復局面,因此UCI協議中規定,局面由棋局的最后一個不可逆局面(發生吃子、進兵或升變的局面)和它的后續著法共同表示,這樣就涵蓋了重復局面的信息。】
           
          舉例說明國際象棋中的諸多棋盤表示方法
           
            國際象棋中有很多表示棋盤的方法,以下這些是程序中可能用到的:
           
            一、棋盤格子的8x8數組
            每個格子的值代表格子中的棋子(例如:enum { empty, wK, wN, wB, wR, wQ, wP, bK, bN, bR, bQ, bP })。它的優點在于:(1) 簡單;(2) 容易計算子力價值:
           
          for (i = 0; i < 8; i ++)
           for( j = 0; j < 8; j ++)
            score += value[square[i, j]];
           
            計算可能的著法有些麻煩但并不非常困難,可以找遍棋盤的每個格子,根據棋子的顏色和類型來做分枝:
           
          for (i = 0; i < 8; i++) {
           for(j = 0; j < 8; j++) {
            switch (board[i, j]) {
            case wP:
             if (board[i + 1, j] = empty) {
              生成到(i + 1, j)的著法;
             }
             if (i == 2 && board[i + 1, j] == empty && board[i + 2, j] empty) {
              生成到(i + 2, j)的著法;
             }
             if (j > 0 && board[i + 1, j - 1]有黑子) {
              生成吃(i + 1, j - 1)子的著法;
             }
             if (j < 7 && board[i + 1, j + 1]有黑子) {
              生成吃(i + 1, j + 1)子的著法;
             }
             break;
             ...
            }
           }
          }
           
            但是很多檢測邊界的判斷(例如,兵在車一路就不能吃更外側的子),使得代碼非常復雜,速度非常緩慢。
           
            二、擴展數組
            包括擴展邊界的10x10數組,在棋子類型中應包括“boundary”這個值。這樣就可以花很少的代價,來簡化一些判斷。【在下面提到的0x88方法問世以前,最普遍的做法卻是用12x12的數組(即有雙重的擴展邊界),這樣連馬的著法也不用擔心跳出棋盤了。】
           
            三、0x88
            這個名稱來自一個技巧——通過判斷格子編碼是否包含136這個數(16進制中是0x88)來檢測著法是否合理。下表就是格子的編碼(用一個字節),高4位表示行,低4位表示列。
          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個格子的數組(其中64格代表棋盤上存在的格子),這種表示的優勢在于:(1) 數組僅用1個指標,這樣寫的程序要比用2個指標快;(2) 可以快速簡單地判斷某個格子i是否在棋盤上——當且僅當(i & 0x88) == 0i在棋盤上。(因為列超出范圍時i & 0x08不為零,而行超出范圍時i & 0x80不為零。)這是一個非常有效而且常用的技巧。
           
            四、位棋盤
            我必須在這里多花些筆墨,因為這個技術還不被人所熟悉,但是我認為它可能會很好用的。以前用一個格子數組,每個元素含有一個棋子,現在取而代之的是一個棋子數組,每個元素代表棋盤上哪幾個格子有這個棋子,按位壓縮表示。由于棋盤上有64個格子,所以棋盤可以壓縮成一個64位的字(即兩個32位的字)。這種表示最大的優勢在于,可以通過布爾操作(位操作)來加快局面評估和著法生成操作的速度。試想一下,如此煩瑣的事情可以用壓縮的字運算來解決,例如下面的局面:
           
           
            白方的兵(這個64位數字記作wP)應該由這些位構成:
           
          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
           
            而被黑方占據的格子可以用下面的公式來計算:
           
          bOcc = bP | bN | bB | bR | bQ | bK
           
            (這里bP等等代表黑方不同兵種的棋子),類似地可以計算白方占據的格子,還可以把這兩個格子作“或”運算得到所有被占的格子。這些白兵前進一格可能到達的格子,可以用下面的公式計算:
           
          single_pawn_moves = (wP << 8) & ~occupied
           
            現在仔細看一下過程,先將wP左移8位,來產生每個兵前面的格子:
           
          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 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
           
            對以上兩個位棋盤按位作“與”運算,就得到這些兵前面的沒有被占的格子了:
           
          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
           
            參照兵走一格的方法,可以找到兵走兩格的著法,即再左移8位,“與”上沒有子的格子,再“與”上一個只有第四行都占滿的常數棋盤(這是兵走兩格能到達的唯一一行)
           
          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
           
            值得注意的是,這個常數棋盤是在編譯的時候生成的,而不需要在每次著法生成的時候再計算出來。兵吃子的情況也類似,先左移7位或9位,再“與”上一個常數棋盤以過濾左邊線或右邊線的格子【對于左移7位產生吃右邊子時,需要考慮子在右邊線的情況,此時左移7位是同一行的左邊線,因此需要排除這個格子,即“與”上一個常數棋盤,代表除左邊線以外的所有格子】,最后“與”上bOcc【前面提到過這個棋盤,代表黑子所占的格子】。
            位棋盤這個技術不能簡化代碼,但是它能一次就產生兵的所有著法,而不是一次只產生一個著法。此外,這個過程中有些表達式需要多次反復使用(例如bOcc),但只要計算一次就可以了。因此,位棋盤最終變得非常高效,在棋子數比國際象棋少的棋類中,我想位棋盤的效果會更好。
            有個復雜的問題產生了:計算位棋盤中有多少非零位,或者找到【最低或最高的】一個非零位(例如把兵可以到達的位棋盤轉化為一系列著法),這些操作往往非常重要。計算位數的操作可以一次計算一個字節,做一個長度為256的數組,每個元素代表它的指標所含有多少個非零位,然后通過查表來完成。在找到一個非零位的計算中有個技巧:x ^ (x - 1)(^”代表異或),會得到一個二進制為...000111...的數,x ^ (x - 1)的第一位就是x的最后一位非零位。如果要求得這個數字x ^ (x - 1),即型如...000111...的數】的第一位,就把它對某個特定的數M取余數(不同的...000111...這樣的數對M取余數必須不同),然后去查表。例如,以下的代碼可以找到一個字節的最后一個非零位:
           
          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];
          }
           
            【把原作者提到的這個技巧擴展到16位或32位的情況,不妨可以試探一下(只要找得到合適的除數即可)。但是原作者沒有充分考慮計算機的運算特點,因此譯者認為這不是一個合理的設計。由于“電腦一做除法就成了傻瓜”,所以代碼中取余數的過程嚴重影響了效率,為此譯者收集了一些使用炫技的程序,可以迅速完成求位數和查找位的操作。
            (1) 求一個32位數中有幾位非零位的運算——Count32操作:
           
          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);
          }
           
            (2) 求最低位非零位是第幾位的運算——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;
          }
           
            (3) 求最高位非零位是第幾位的運算——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;
          }
           
            對64位數字進行操作,把它分解成兩個32位字,分別對兩個字調用上面的函數就可以了。如果程序能運行在64位的處理器上,只要對上面三個函數略加改動就可以了?!?/font>
           
          如何撤消著法?
           
            還記得嗎?我們說過在棋盤表示方法中需要涉及撤消著法的操作?,F在有兩種解決方案:(1) 用一個堆棧來保存棋盤,執行一個著法前先將棋盤壓入堆棧,撤消著法就從堆棧彈出棋盤,恐怕這太慢了…… (2) 用一個堆棧來保存著法,執行一個著法前先將該著法及其相關信息壓入堆棧,撤消著法就根據該著法還原棋盤及其相關信息。例如,在國際象棋中只要保存被吃的棋子(如果吃子的話),以及王車易位和吃過路兵的權利等信息。
           
          重復檢測
           
            某些棋類在發生重復局面時要用到特殊的規則,如圍棋和國際象棋(在國際象棋中,第三次出現重復局面時,制造重復局面的一方就有權宣布和棋)。那么如何知道是否出現重復局面呢?答案很簡單:用散列函數把局面轉換成一個相當大的數(我們以后要談到這個問題,因為這個技術還可以加快搜索速度),然后保存一系列前面出現過的局面的散列值,從這些值里面找就是了。一個典型的散列函數,先隨機產生一張64x13的表,如果棋子y在位置x上,就把表中[x, y]這個數加到散列值上(忽略溢出)[Zobrist]。值得注意的是,當棋子y從位置x走到位置z時,可以快速地更新散列值:減去[x, y]并加上[z, y]即可。

          posted on 2007-08-10 17:21 小鋒 閱讀(679) 評論(0)  編輯  收藏 所屬分類: algorithm

          主站蜘蛛池模板: 泰顺县| 仪征市| 永平县| 阳新县| 南丰县| 河西区| 尼木县| 通道| 侯马市| 济南市| 河源市| 弥渡县| 华宁县| 木里| 阳曲县| 阿拉善左旗| 平泉县| 沙河市| 巴彦淖尔市| 定结县| 拉萨市| 施甸县| 漳州市| 凤城市| 环江| 临沂市| 富蕴县| 阳山县| 和硕县| 南澳县| 顺义区| 颍上县| 华亭县| 德州市| 德钦县| 松桃| 石城县| 佛山市| 阳江市| 修文县| 泾阳县|