小编典典

类De Bruijn的“ 2 ^ n-1”序列:如何构造?

algorithm

我正在查看条目通过Bit
Twiddling
hacks中的
乘法和查找在O(lg(N))操作中找到N位整数的对数2

我可以轻松地看到该条目中第二个算法的工作原理

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呢?我觉得这里缺少明显的东西。

PS
为了澄清我的问题:我并不是真的在寻找一种算法来生成这些序列。我对某种或多或少的琐碎属性(如果有的话)感兴趣,这些属性可以0x07C4ACDD按我们希望的那样起作用。对于0x077CB531使它起作用的属性而言,它是显而易见的:它包含“存储”在序列中的所有5位组合,并带有1位步进(这基本上就是De
Bruijn序列)。

0x07C4ACDD,而另一方面,是不是本身就是一个德布鲁因序列。那么,它们在构造时的目标是什么性质0x07C4ACDD(除了非构造性的“它应该使上述算法起作用”)?有人确实以某种方式提出了上述算法。因此,他们可能知道该方法是可行的,并且存在适当的顺序。他们怎么知道的?

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

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

第一。然后,我要做的++v是将其v转换为2的幂(假设它没有溢出)。然后,我将应用第一个算法。最后,我将做--r以获得最终答案。但是,这些人设法对其进行了优化:他们仅通过更改乘数并重新排列表格就消除了前导++v和后继--r步骤。他们怎么知道这是可能的?此优化背后的数学原理是什么?


阅读 428

收藏
2020-07-28

共1个答案

小编典典

在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乘幂的唯一数。然后,您只需要一个查找表即可确定它是哪一个。它的工作原理类似于数字小于1的2的幂,但是在这种情况下,幻数不是De
Bruijn序列,而是类似物。定义属性只需更改为“

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

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

我这样做的方法是,使用64元素的查找表而不是32的查找表,生成随机幻数,然后针对其中的每一个,使用两个输入的幂建立查找表,检查其正确性(内射性),然后对其进行验证所有32位数字。我继续直到遇到正确的幻数。由于只有33个数字出现,所以所得数字不具有“出现每个可能的n长度字”的属性,这对于所有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();
    }
}
2020-07-28