“2 ^ n - 1”的De Bruijn式序列:它是如何构建的?

时间:2011-09-09 17:57:04

标签: algorithm bit-manipulation discrete-mathematics logarithm

我正在查看来自Find the log base 2 of an N-bit integer in O(lg(N)) operations with multiply and lookup的条目Bit Twiddling hacks

我可以很容易地看到该条目中的第二个算法如何工作

static const int MultiplyDeBruijnBitPosition2[32] = 
{
  0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 
  31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition2[(uint32_t)(v * 0x077CB531U) >> 27];

计算n = log2 v,其中v已知为2的幂。在这种情况下,0x077CB531是一个普通的De Bruijn序列,其余的是显而易见的。

然而,该条目中的第一个算法

static const int MultiplyDeBruijnBitPosition[32] =
{
  0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
  8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
};

v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;

r = MultiplyDeBruijnBitPosition[(uint32_t)(v * 0x07C4ACDDU) >> 27];

对我来说看起来有点棘手。我们首先将v捕捉到最接近的更大2^n - 1值。然后将此2^n - 1值乘以0x07C4ACDD,在这种情况下,其行为与先前算法中的DeBruijn序列的行为相同。

我的问题是:我们如何构建这个神奇的0x07C4ACDD序列?即我们如何构造一个序列,当乘以2^n - 1值时,该序列可用于生成唯一索引?对于2^n乘数,它只是一个普通的De Bruijn序列,正如我们在上面所看到的那样,所以很明显0x077CB531来自哪里。但是2^n - 1乘数0x07C4ACDD呢?我觉得我在这里遗漏了一些明显的东西。

P.S。为了澄清我的问题:我并不是在寻找生成这些序列的算法。我对一些或多或少的琐碎属性(如果存在的话)更感兴趣,这使得0x07C4ACDD能够按照我们希望的方式工作。对于0x077CB531,使其工作的属性非常明显:它包含序列中“存储”的所有5位组合,具有1位步进(基本上是De Bruijn序列)。

另一方面,0x07C4ACDD本身并不是De Bruijn序列。那么,在构建0x07C4ACDD时他们的目标是什么(除了非建设性的“它应该使上述算法工作”)?有人确实以某种方式提出了上述算法。所以他们可能知道这种方法是可行的,并且存在适当的序列。他们怎么知道的?

例如,如果我要为任意v构建算法,我会做

v |= v >> 1;
v |= v >> 2;
...

第一。然后我只需要++vv转换为2的幂(让我们假设它不会溢出)。然后我会应用第一个算法。最后我会--r来获得最终答案。但是,这些人设法对其进行优化:他们只需更改乘数并重新排列表格即可消除前导++v和后续--r步骤。他们怎么知道这是可能的?这种优化背后的数学是什么?

3 个答案:

答案 0 :(得分:14)

在k个符号(和k ^ n长度)上的n阶的De Bruijn序列具有这样的属性:每个可能的n长度字在其中显示为连续字符,其中一些具有循环包装。例如,在k = 2,n = 2的情况下,可能的单词是00,01,10,11,并且De Bruijn序列是0011.00,01,11出现在其中,10表示包裹。这个属性自然意味着左移De Bruijn序列(乘以2的幂)并取其高n位导致两个乘法器的每个幂的唯一数。然后,您只需要一个查找表来确定它是哪一个。它的工作原理类似于数字,其数值小于2的幂,但在这种情况下,幻数不是De Bruijn序列,而是类比。定义属性简单地改变为“每个可能的n长度的单词出现为长度为n的前m个子序列的总和,mod 2 ^ n”。此属性是算法工作所需的全部属性。他们只是使用这种不同类型的幻数来加速算法。我做得也好。

构建De Bruijn数的一种可能方法是生成De Bruijn图的哈密顿路径,维基百科提供了这种图的示例。在这种情况下,节点是2 ^ 5 = 32位整数,有向边是它们之间的过渡,其中过渡是左移,二进制或操作根据边的标签,0或1。它可能是2 ^ n-1类型幻数的直接类比,值得探索,但这并不是人们通常构造此类算法的方法。

在实践中,您可能会尝试以不同的方式构造它,特别是如果您希望它以不同的方式运行。例如,在bit twiddling hacks页面上实现前导/尾随数量的零算法只能返回[0..31]中的值。它需要额外检查0的情况,它有32个零。这种检查需要分支,在某些CPU上可能太慢。

我这样做的时候,我使用了64个元素的查找表而不是32个,生成了随机幻数,并为每一个我建立了一个具有两个输入功率的查找表,检查了它的正确性(注入性),然后验证了所有32位数字。我继续,直到遇到一个正确的幻数。结果数字不符合“每个可能的n长度单词出现”的属性,因为只显示33个数字,这对于所有33个可能的输入都是唯一的。

详尽的强力搜索听起来很慢,特别是如果好的魔术数字很少,但如果我们首先测试两个值的已知功率作为输入,则表格快速填充,拒绝速度快,拒绝率非常高。我们只需要在每个幻数后清除表格。本质上,我利用高拒绝率算法来构造幻数。

生成的算法

int32 Integer::numberOfLeadingZeros (int32 x)
{
    static int32 v[64] = {
        32, -1, 1, 19, -1, -1, -1, 27, -1, 24, 3, -1, 29, -1, 9, -1,
        12, 7, -1, 20, -1, -1, 4, 30, 10, -1, 21, -1, 5, 31, -1, -1,
        -1, -1, 0, 18, 17, 16, -1, -1, 15, -1, -1, -1, 26, -1, 14, -1,
        23, -1, 2, -1, -1, 28, 25, -1, -1, 13, 8, -1, -1, 11, 22, 6};
    x |= x >> 1;
    x |= x >> 2;
    x |= x >> 4;
    x |= x >> 8;
    x |= x >> 16;
    x *= 0x749c0b5d;
    return v[cast<uint32>(x) >> 26];
}

int32 Integer::numberOfTrailingZeros (int32 x)
{
    static int32 v[64] = {
        32, -1, 2, -1, 3, -1, -1, -1, -1, 4, -1, 17, 13, -1, -1, 7,
        0, -1, -1, 5, -1, -1, 27, 18, 29, 14, 24, -1, -1, 20, 8, -1,
        31, 1, -1, -1, -1, 16, 12, 6, -1, -1, -1, 26, 28, 23, 19, -1,
        30, -1, 15, 11, -1, 25, 22, -1, -1, 10, -1, 21, 9, -1, -1, -1};
    x &= -x;
    x *= 0x4279976b;
    return v[cast<uint32>(x) >> 26];
}

关于他们如何知道的问题,他们可能没有。他们试验,试图改变一切,就像我一样。毕竟,2 ^ n-1输入可能工作而不是2 ^ n输入具有不同的幻数和查找表,这不是一大片想象。

在这里,我制作了我的幻数生成器代码的简化版本。如果我们只检查两个输入的功率,它会在5分钟内检查所有可能的幻数,找到1024个幻数。检查其他输入是没有意义的,因为无论如何它们都减少到2 ^ n-1形式。不构造表格,但一旦你知道了幻数,它就是微不足道的。

#include <Frigo/all>
#include <Frigo/all.cpp>

using namespace Frigo::Lang;
using namespace std;

class MagicNumberGenerator
{

    public:

        static const int32 log2n = 5;
        static const int32 n = 1 << log2n;
        static const bool tryZero = false;

        MagicNumberGenerator () {}

        void tryAllMagic ()
        {
            for( int32 magic = 0; magic < Integer::MAX_VALUE; magic++ ){
                tryMagic(magic);
            }
            tryMagic(Integer::MAX_VALUE);
            for( int32 magic = Integer::MIN_VALUE; magic < 0; magic++ ){
                tryMagic(magic);
            }
        }

        bool tryMagic (int32 magic)
        {
            //  clear table
            for( int32 i = 0; i < n; i++ ){
                table[i] = -1;
            }
            //  try for zero
            if( tryZero and not tryInput(magic, 0) ){
                return false;
            }
            //  try for all power of two inputs, filling table quickly in the process
            for( int32 i = 0; i < 32; i++ ){
                if( not tryInput(magic, 1 << i) ){
                    return false;
                }
            }
            //  here we would test all possible 32-bit inputs except zero, but it is pointless due to the reduction to 2^n-1 form
            //  we found a magic number
            cout << "Magic number found: 0x" << Integer::toHexString(magic) << endl;
            return true;
        }

        bool tryInput (int32 magic, int32 x)
        {
            //  calculate good answer
            int32 leadingZeros = goodNumberOfLeadingZeros(x);
            //  calculate scrambled but hopefully injective answer
            x |= x >> 1;
            x |= x >> 2;
            x |= x >> 4;
            x |= x >> 8;
            x |= x >> 16;
            x *= magic;
            x = Integer::unsignedRightShift(x, 32 - log2n);
            //  reject if answer is not injective
            if( table[x] != -1 ){
                return table[x] == leadingZeros;
            }
            //  store result for further injectivity checks
            table[x] = leadingZeros;
            return true;
        }

        static int32 goodNumberOfLeadingZeros (int32 x)
        {
            int32 r = 32;
            if( cast<uint32>(x) & 0xffff0000 ){
                x >>= 16;
                r -= 16;
            }
            if( x & 0xff00 ){
                x >>= 8;
                r -= 8;
            }
            if( x & 0xf0 ){
                x >>= 4;
                r -= 4;
            }
            if( x & 0xc ){
                x >>= 2;
                r -= 2;
            }
            if( x & 0x2 ){
                x >>= 1;
                r--;
            }
            if( x & 0x1 ){
                r--;
            }
            return r;
        }

        int32 table[n];

};

int32 main (int32 argc, char* argv[])
{
    if(argc||argv){}
    measure{
        MagicNumberGenerator gen;
        gen.tryAllMagic();
    }
}

答案 1 :(得分:3)

它基于论文Using de Bruijn Sequences to Index a 1 in a Computer Word。我猜他们会搜索一个完美的哈希函数来将2^n-1映射到[0..31]。他们描述了一种搜索整数计数零的方法,最多设置两位,包括逐步建立乘数。

答案 2 :(得分:3)

来自:http://www.stmintz.com/ccc/index.php?id=306404

130329821
0x07C4ACDD
00000111110001001010110011011101B

bit 31 - bit 27   00000  0
bit 30 - bit 26   00001  1
bit 29 - bit 25   00011  3
bit 28 - bit 24   00111  7
bit 27 - bit 23   01111 15
bit 26 - bit 22   11111 31
bit 25 - bit 21   11110 30
bit 24 - bit 20   11100 28
bit 23 - bit 19   11000 24
bit 22 - bit 18   10001 17
bit 21 - bit 17   00010  2
bit 20 - bit 16   00100  4
bit 19 - bit 15   01001  9
bit 18 - bit 14   10010 18
bit 17 - bit 13   00101  5
bit 16 - bit 12   01010 10
bit 15 - bit 11   10101 21
bit 14 - bit 10   01011 11
bit 13 - bit  9   10110 22
bit 12 - bit  8   01100 12
bit 11 - bit  7   11001 25
bit 10 - bit  6   10011 19
bit  9 - bit  5   00110  6
bit  8 - bit  4   01101 13
bit  7 - bit  3   11011 27
bit  6 - bit  2   10111 23
bit  5 - bit  1   01110 14
bit  4 - bit  0   11101 29
bit  3 - bit 31   11010 26 
bit  2 - bit 30   10100 20
bit  1 - bit 29   01000  8
bit  0 - bit 28   10000 16

在我看来,0x07C4ACDD是一个5位de Bruijn序列。