-
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) == 0时i在棋盘上。(因为列超出范围时i & 0x08不为零,而行超出范围时i & 0x80不为零。)这是一个非常有效而且常用的技巧。
- 四、位棋盘
- 我必须在这里多花些笔墨,因为这个技术还不被人所熟悉,但是我认为它可能会很好用的。以前用一个格子数组,每个元素含有一个棋子,现在取而代之的是一个棋子数组,每个元素代表棋盘上哪几个格子有这个棋子,按位压缩表示。由于棋盘上有64个格子,所以棋盘可以压缩成一个64位的字(即两个32位的字)。这种表示最大的优势在于,可以通过布尔操作(位操作)来加快局面评估和着法生成操作的速度。试想一下,如此烦琐的事情可以用压缩的字运算来解决,例如下面的局面:
- 白方的兵(这个64位数字记作wP)应该由这些位构成:
- 这样,格子i的左边一格是i - 1,右边是i + 1,上边是i + 16,下边是i - 16,等等。棋盘被表示为128个格子的数组(其中64格代表棋盘上存在的格子),这种表示的优势在于:(1) 数组仅用1个指标,这样写的程序要比用2个指标快;(2) 可以快速简单地判断某个格子i是否在棋盘上——当且仅当(i & 0x88) == 0时i在棋盘上。(因为列超出范围时i & 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
-
- 而被黑方占据的格子可以用下面的公式来计算:
- 而被黑方占据的格子可以用下面的公式来计算:
- bOcc = bP | bN | bB | bR | bQ | bK
-
- (这里bP等等代表黑方不同兵种的棋子),类似地可以计算白方占据的格子,还可以把这两个格子作“或”运算得到所有被占的格子。这些白兵前进一格可能到达的格子,可以用下面的公式计算:
- (这里bP等等代表黑方不同兵种的棋子),类似地可以计算白方占据的格子,还可以把这两个格子作“或”运算得到所有被占的格子。这些白兵前进一格可能到达的格子,可以用下面的公式计算:
- single_pawn_moves = (wP << 8) & ~occupied
-
- 现在仔细看一下过程,先将wP左移8位,来产生每个兵前面的格子:
- 现在仔细看一下过程,先将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 0 0 0 1 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
- 1 0 1 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 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位,“与”上没有子的格子,再“与”上一个只有第四行都占满的常数棋盘(这是兵走两格能到达的唯一一行):
- 参照兵走一格的方法,可以找到兵走两格的着法,即再左移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
- 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取余数必须不同),然后去查表。例如,以下的代码可以找到一个字节的最后一个非零位:
- 值得注意的是,这个常数棋盘是在编译的时候生成的,而不需要在每次着法生成的时候再计算出来。兵吃子的情况也类似,先左移7位或9位,再“与”上一个常数棋盘以过滤左边线或右边线的格子【对于左移7位产生吃右边子时,需要考虑子在右边线的情况,此时左移7位是同一行的左边线,因此需要排除这个格子,即“与”上一个常数棋盘,代表除左边线以外的所有格子】,最后“与”上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 };
-
- 【把原作者提到的这个技巧扩展到16位或32位的情况,不妨可以试探一下(只要找得到合适的除数即可)。但是原作者没有充分考虑计算机的运算特点,因此译者认为这不是一个合理的设计。由于“电脑一做除法就成了傻瓜”,所以代码中取余数的过程严重影响了效率,为此译者收集了一些使用炫技的程序,可以迅速完成求位数和查找位的操作。
- (1) 求一个32位数中有几位非零位的运算——Count32操作:
- 【把原作者提到的这个技巧扩展到16位或32位的情况,不妨可以试探一下(只要找得到合适的除数即可)。但是原作者没有充分考虑计算机的运算特点,因此译者认为这不是一个合理的设计。由于“电脑一做除法就成了傻瓜”,所以代码中取余数的过程严重影响了效率,为此译者收集了一些使用炫技的程序,可以迅速完成求位数和查找位的操作。
-
- 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) 求最低位非零位是第几位的运算——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) 求最高位非零位是第几位的运算——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位数字进行操作,把它分解成两个32位字,分别对两个字调用上面的函数就可以了。如果程序能运行在64位的处理器上,只要对上面三个函数略加改动就可以了。】
- 如何撤消着法?
- 还记得吗?我们说过在棋盘表示方法中需要涉及撤消着法的操作。现在有两种解决方案:(1) 用一个堆栈来保存棋盘,执行一个着法前先将棋盘压入堆栈,撤消着法就从堆栈弹出棋盘,恐怕这太慢了…… (2) 用一个堆栈来保存着法,执行一个着法前先将该着法及其相关信息压入堆栈,撤消着法就根据该着法还原棋盘及其相关信息。例如,在国际象棋中只要保存被吃的棋子(如果吃子的话),以及王车易位和吃过路兵的权利等信息。
- 重复检测
- 某些棋类在发生重复局面时要用到特殊的规则,如围棋和国际象棋(在国际象棋中,第三次出现重复局面时,制造重复局面的一方就有权宣布和棋)。那么如何知道是否出现重复局面呢?答案很简单:用散列函数把局面转换成一个相当大的数(我们以后要谈到这个问题,因为这个技术还可以加快搜索速度),然后保存一系列前面出现过的局面的散列值,从这些值里面找就是了。一个典型的散列函数,先随机产生一张64x13的表,如果棋子y在位置x上,就把表中[x, y]这个数加到散列值上(忽略溢出)[即Zobrist值]。值得注意的是,当棋子y从位置x走到位置z时,可以快速地更新散列值:减去[x, y]并加上[z, y]即可。
- 原文:
- 译者:黄晨 (webmaster@{域名已经过期})
- 类型:全译加译注
- 对64位数字进行操作,把它分解成两个32位字,分别对两个字调用上面的函数就可以了。如果程序能运行在64位的处理器上,只要对上面三个函数略加改动就可以了。】
栏目列表
棋盘的表示
发布时间:2008-11-20
点击:
来源:本站原创
录入者:David Eppstein
附件:
![]() ![]() |