【问题标题】:How to find magic bitboards?如何找到魔术位板?
【发布时间】:2015-06-06 08:16:01
【问题描述】:
const int BitTable[64] = {
  63, 30, 3, 32, 25, 41, 22, 33, 15, 50, 42, 13, 11, 53, 19, 34, 61, 29, 2,
  51, 21, 43, 45, 10, 18, 47, 1, 54, 9, 57, 0, 35, 62, 31, 40, 4, 49, 5, 52,
  26, 60, 6, 23, 44, 46, 27, 56, 16, 7, 39, 48, 24, 59, 14, 12, 55, 38, 28,
  58, 20, 37, 17, 36, 8
};

int pop_1st_bit(uint64 *bb) {
  uint64 b = *bb ^ (*bb - 1);
  unsigned int fold = (unsigned) ((b & 0xffffffff) ^ (b >> 32));
  *bb &= (*bb - 1);
  return BitTable[(fold * 0x783a9b23) >> 26];
}

uint64 index_to_uint64(int index, int bits, uint64 m) {
  int i, j;
  uint64 result = 0ULL;
  for(i = 0; i < bits; i++) {
    j = pop_1st_bit(&m);
    if(index & (1 << i)) result |= (1ULL << j);
  }
  return result;
}

来自国际象棋编程维基:
https://www.chessprogramming.org/Looking_for_Magics

这是查找magic numbers 的一些代码的一部分。

参数uint64 m 是一个bitboard,表示车或象移动可能的阻塞方格。以 e4 方格上的车为例:

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 0 1 0 0 0 
0 1 1 1 0 1 1 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 0 0 0 0 

边缘方块为零,因为它们总是阻塞,减少所需的位数显然很有帮助。

/* Bitboard, LSB to MSB, a1 through h8:
 * 56 - - - - - - 63
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  0 - - - - - - 7
 */

所以在上面的例子中,index_to_uint64 采用一个索引(0 到 2^bits),位板中设置的位数(10),以及位板。

然后pops_1st_bit 对应每个位数,然后是另一个移位的代码位。 pops_1st_bit 将位板与自身减一进行异或(为什么?)。然后它与一个完整的 32 位进行 AND 运算,我的大脑在这里的某个地方耗尽了 RAM。不知何故,神奇的十六进制数字 0x783a9b23 被牵扯进来(这是《迷失》中的数字序列吗?)。还有这个由 0-63 随机排列的数字组成的荒谬神秘数组 (BitTable[64])。

【问题讨论】:

  • 对于喜欢冒险的人来说,这是一篇有用的文章——vicki-chess.blogspot.com/2013/04/magics.html——我认为有问题的代码正在计算第 2 步,尽管我没有不明白怎么做。
  • pop_1st_bit 取消设置最低位并返回其索引
  • 数字 0x783a9b23 是素数,因此使 BitTable 成为哈希。您可能最好将其视为黑匣子,只注意“if number in = x then number out = y”。
  • 该代码似乎是基于 deBruijn 序列的方法的变体,用于查找数字的 LSB。我不确定这个常数是如何工作的。从外观上看,这种方法的发明者马特泰勒(Matt Taylor)也没有(或者至少在他发现它时没有回来)groups.google.com/forum/#!topic/comp.lang.asm.x86/3pVGzQGb1ys。顺便说一句,Droidfish implements this 在 64 位机器上使用 deBruijn 序列,在 32 位机器上回退到上述序列。

标签: c bit-manipulation chess


【解决方案1】:

好的,我想通了。

首先,一些术语:

blocker mask:一个位板,包含所有可以阻挡一个棋子的方格,对于给定的棋子类型和棋子所在的方格。它不包括终止边方块,因为它们总是阻塞。

blocker board:包含被占用的方块的位板。它只有方块,它们也在阻挡掩码中。

移动棋盘:包含棋子可以移动到的所有格子的位棋盘,给定棋子类型、格子和挡板。如果棋子可以移动到那里,它包括终止边缘方块。

以 e4 方格上的车为例,e2、e5、e7、b4 和 c4 上有一些随机棋子。

 The blocker mask        A blocker board         The move board
 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 1 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         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 0 1 0 0 0 
 0 1 1 1 0 1 1 0         0 1 1 0 0 0 0 0         0 0 1 1 0 1 1 1 
 0 0 0 0 1 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 0 1 0 0 0         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 0 0 0 

注意事项:

  • 对于给定的方块和棋子类型(车或象),阻挡器掩码始终相同。
  • 阻隔板包括友方棋子和敌方棋子,它是阻隔掩码的子集。
  • 生成的棋盘可能包含捕获你自己棋子的棋步,但之后这些棋步很容易被移除:moveboard &amp;= ~friendly_pieces)

ma​​gic numbers 方法的目标是非常快速地查找给定 blocker board 的预先计算好的 move board。否则,您每次都必须(慢慢地)计算移动板。这仅适用于滑动件,即车和主教。女王只是车和主教的结合体。

可以为每个方块和棋子类型的组合找到幻数。为此,您必须计算每个方块/棋子组合的每个可能的挡板变化。这就是有问题的代码正在做的事情。 如何对我来说仍然是一个谜,但that also seems to be the case for the apparent original author, Matt Taylor。 (感谢@Pradhan 提供链接)

所以我所做的是重新实现代码以生成所有可能的拦截器板变体。它使用了不同的技术,虽然速度有点慢,但更容易阅读和理解。它稍慢的事实不是问题,因为此代码对速度不是很关键。程序只需在程序启动时执行一次,在双核 i5 上只需几微秒。

/* Generate a unique blocker board, given an index (0..2^bits) and the blocker mask 
 * for the piece/square. Each index will give a unique blocker board. */
static uint64_t gen_blockerboard (int index, uint64_t blockermask) 
{
    /* Start with a blockerboard identical to the mask. */
    uint64_t blockerboard = blockermask;

    /* Loop through the blockermask to find the indices of all set bits. */
    int8_t bitindex = 0;
    for (int8_t i=0; i<64; i++) {
        /* Check if the i'th bit is set in the mask (and thus a potential blocker). */
        if ( blockermask & (1ULL<<i) ) {
            /* Clear the i'th bit in the blockerboard if it's clear in the index at bitindex. */
            if ( !(index & (1<<bitindex)) ) {
                blockerboard &= ~(1ULL<<i); //Clear the bit.
            }
            /* Increment the bit index in the 0-4096 index, so each bit in index will correspond 
             * to each set bit in blockermask. */
            bitindex++;
        }
    }
    return blockerboard;
}

要使用它,请执行以下操作:

int bits = count_bits( RookBlockermask[square] );
/* Generate all (2^bits) blocker boards. */ 
for (int i=0; i < (1<<bits); i++) {
    RookBlockerboard[square][i] = gen_blockerboard( i, RookBlockermask[square] );
}

它是如何工作的:有 2^bits 阻塞板,其中 bits 是阻塞掩码中 1 的数量,这是唯一相关的位。此外,从 0 到 2^bits 的每个整数都有一个唯一的 1 和 0 序列,长度为 bits。所以这个函数只是将给定整数中的每个位对应到阻塞掩码中的相关位,并相应地关闭/打开它以生成一个唯一的阻塞板。

它没有那么聪明或快速,但它是可读的。

【讨论】:

    【解决方案2】:

    好的,我将尝试逐步完成。

    index_to_uint64( 7, 10, m ); 
    

    7 只是一个在 0 到 2^10 之间随机选择的数字,而 10 是在 m 中设置的位数。 m 可以用四种方式表示:

    bitboard:
    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 0 1 0 0 0 
    0 1 1 1 0 1 1 0 
    0 0 0 0 1 0 0 0 
    0 0 0 0 1 0 0 0 
    0 0 0 0 0 0 0 0 
    dec: 4521262379438080
    hex: 0x1010106e101000
    bin: 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0001 0000 0000 0000
    

    继续前进。这将被调用 10 次。它有一个返回值,它修改了 m。

    pop_1st_bit(&m);
    

    在pop_1st_bit 中,m 由bb 引用。为了清楚起见,我将其更改为 m。

    uint64 b = m^(m-1);
    

    m-1 部分获取设置的最低有效位并将其翻转以及它下面的所有位。在异或之后,所有这些更改的位现在都设置为 1,而所有高位都设置为 0。

    m  : 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0001 0000 0000 0000 
    m-1: 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0000 1111 1111 1111
    b  : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111
    

    下一步:

    unsigned int fold = (unsigned) ((b & 0xffffffff) ^ (b >> 32));
    

    (b &amp; 0xffffffff) 部分与 b 与低 32 个设置位。所以这基本上清除了 b 上半部分的所有位。

    (b & 0xffffffff)
    b: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111
    &: 0000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111
    =: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111
    

    ... ^ (b &gt;&gt; 32) 部分将 b 的上半部分移动到下半部分,然后将其与前一个操作的结果进行异或。所以它基本上将 b 的上半部分与 b 的下半部分进行异或。这在这种情况下无效,因为 b 的上半部分一开始是空的。

    >> :0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 
    ^  :0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111 
    
    uint fold = 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111
    

    我不明白“折叠”的意义,即使在 b 的上半部分设置了位。

    不管怎样,继续。下一行实际上通过取消设置最低位来修改 m。这有点道理。

    m &= (m - 1);
    m  : 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0001 0000 0000 0000 
    m-1: 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0000 1111 1111 1111
    &  : 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0000 0000 0000 0000 
    

    下一部分将fold 乘以某个十六进制数(素数?),将乘积 26 右移,并将其用作 BitTable 的索引,我们神秘的随机排序数字 0-63 数组。此时我怀疑作者可能正在编写一个伪随机数生成器。

    return BitTable[(fold * 0x783a9b23) >> 26];
    

    pop_1st_bit 到此结束。这一切都完成了 10 次(对于最初在 m 中设置的每个位执行一次)。对 pop_1st_bit 的 10 次调用中的每一次都返回一个数字 0-63。

    j = pop_1st_bit(&m);
    if(index & (1 << i)) result |= (1ULL << j);
    

    在上面两行中,i 是我们所在的当前位,0-9。因此,如果index 数字(最初作为参数传递给 index_to_uint64 的 7)设置了第 i 位,则在结果中设置第 j 位,其中 j 是 pop_1st_bit 的 0-63 返回值。

    就是这样!我还是一头雾水:(

    【讨论】:

    • 也许感兴趣:BitTable 中的所有数字只出现一次,在0..63 范围内。这表明正在发生一些重新排序。查看index_to_uint64 在其余代码中的使用位置。
    【解决方案3】:

    在 youtube 上观看有关国际象棋引擎的视频系列时,我遇到了与 paulwal222 完全相同的问题。似乎涉及到一些高级数学。解释这个困难主题背景的最佳链接是 https://chessprogramming.wikispaces.com/Matt+Taylorhttps://chessprogramming.wikispaces.com/BitScan 。似乎 Matt Taylor 在 2003 年在 google.group (https://groups.google.com/forum/#!topic/comp.lang.asm.x86/3pVGzQGb1ys) 中(也由 pradhan 发现)提出了现在称为 Matt Taylor 折叠技巧的东西,这是一种 32 位友好的实现来查找 LS1B 的位索引(https://en.wikipedia.org/wiki/Find_first_set)。 Taylor 的折叠技巧显然是对 De Bruijn (https://en.wikipedia.org/wiki/Nicolaas_Govert_de_Bruijn) 位扫描的改编,根据 Martin Läuter 的 Donald Knuth 于 1997 年设计的通过最小完美散列 (https://en.wikipedia.org/wiki/Perfect_hash_function) 确定 LS1B 索引。 BitTable 的数字 (63, 30, ..) 和 PopBit 中的折叠 (0x783a9b23) 可能是与 Matt Taylor 的 32 位折叠技巧相关的所谓幻数(唯一?)。这种折叠技巧似乎很快,因为很多引擎都复制了这种方法(f.i Stockfish)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-03
      • 2020-04-18
      • 2021-01-01
      • 2014-09-09
      • 1970-01-01
      相关资源
      最近更新 更多