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