30

我正在查看条目Find the log base 2 of an N-bit integer in O(lg(N)) operations with multiply and lookup from 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 vwherev是已知的 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呢?我觉得我在这里遗漏了一些明显的东西。

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

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

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

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

第一的。然后我会++v变成v2 的幂(假设它不会溢出)。然后我会应用第一个算法。最后我会做--r以获得最终答案。然而,这些人设法对其进行了优化:他们仅通过更改乘数和重新排列表格就消除了前导步++v和尾随步。--r他们怎么知道这是可能的?这种优化背后的数学原理是什么?

4

3 回答 3

16

一个 n 阶超过 k 个符号(并且长度为 k^n 长度)的 De Bruijn 序列具有一个属性,即每个可能的 n 长度单词在其中都显示为连续字符,其中一些具有循环换行。比如在k=2,n=2的情况下,可能的词是00,01,10,11,一个De Bruijn序列是0011。00,01,11出现在里面,10有换行。这个属性自然意味着左移 De Bruijn 序列(乘以 2 的幂)并取其高 n 位会导致每个 2 乘法器的唯一数字。然后你只需要一个查找表来确定它是哪一个。它的工作原理与小于 2 的幂的数字类似,但在这种情况下,幻数不是 De Bruijn 数列,而是一个类比数。定义属性简单地更改为“

构造 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();
    }
}
于 2011-09-10T02:45:39.833 回答
3

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

于 2011-09-10T02:18:18.397 回答
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 序列。

于 2015-11-09T20:46:34.227 回答