有效地找到匹配位掩码的第一个元素
Posted
技术标签:
【中文标题】有效地找到匹配位掩码的第一个元素【英文标题】:efficiently find the first element matching a bit mask 【发布时间】:2012-02-12 02:49:43 【问题描述】:我有一个 N 个 64 位整数的列表,其中的位代表小集合。每个整数最多有 k 位设置为 1。给定一个位掩码,我想找到列表中与掩码匹配的第一个元素,即element & mask == element
。
示例:
如果我的清单是:
index abcdef
0 001100
1 001010
2 001000
3 000100
4 000010
5 000001
6 010000
7 100000
8 000000
而我的掩码是111000
,与掩码匹配的第一个元素位于索引 2。
方法一:
线性搜索整个列表。这需要 O(N) 时间和 O(1) 空间。
方法二:
预先计算所有可能掩码的树,并在每个节点保留该掩码的答案。查询需要 O(1) 时间,但需要 O(2^64) 空间。
问题:
我怎样才能比 O(N) 更快地找到匹配掩码的第一个元素,同时仍然使用合理的空间量?我有能力在预计算上花费多项式时间,因为会有很多查询。关键是 k 很小。在我的应用程序中,k N 数以千计。掩码有很多1;你可以假设它是从 64 位整数的空间中统一绘制的。
更新:
这是一个示例数据集和一个在 Linux 上运行的简单基准程序:http://up.thirld.com/binmask.tar.gz。对于large.in
,N=3779 和 k=3。第一行是 N,后面是 N 表示元素的无符号 64 位整数。使用make
编译。使用./benchmark.e >large.out
运行以创建真正的输出,然后您可以对其进行比较。 (掩码是随机生成的,但随机种子是固定的。)然后将 find_first()
函数替换为您的实现。
简单的线性搜索比我预期的要快得多。这是因为 k 很小,因此对于随机掩码,平均而言很快就能找到匹配项.
【问题讨论】:
是否可以对元素列表进行排序(记录原始索引)?如果对它们进行排序,就会容易。这里有几个地方可以开始考虑这个问题:en.wikipedia.org/wiki/List_of_algorithms#Item_search 和 en.wikipedia.org/wiki/Selection_algorithm(查看“Using_data_structures_to_select_in_sublinear_time”部分)。 当然,我们可以对列表进行排序并存储原始索引。那么问题就变成了:找到匹配掩码并且索引最小的元素。我仍然不知道如何使用任何传统的搜索算法来做到这一点,因为我们正在寻找与掩码匹配的元素,而不是寻找一个特定的元素。 方法 2 占用O(2^64)
空间,这是恒定的但不切实际。
致 OP:您能否提供一个测试数据集?几百个项目可能就足够了。我猜,搜索掩码可以是随机的。 TIA
@wildplasser 请参阅上面的更新。谢谢雷克斯·克尔;固定。
【参考方案1】:
后缀树(位)可以解决问题,原始优先级位于叶节点:
000000 -> 8
1 -> 5
10 -> 4
100 -> 3
1000 -> 2
10 -> 1
100 -> 0
10000 -> 6
100000 -> 7
如果在掩码中设置了该位,则搜索两个臂,如果没有,则仅搜索 0 臂;你的答案是你在叶节点遇到的最小数量。
您可以通过不按顺序遍历位,而是通过最大可辨别性来改善这一点(略微);在您的示例中,请注意 3 个元素设置了第 2 位,因此您将创建
2:0 0:0 1:0 3:0 4:0 5:0 -> 8
5:1 -> 5
4:1 5:0 -> 4
3:1 4:0 5:0 -> 3
1:1 3:0 4:0 5:0 -> 6
0:1 1:0 3:0 4:0 5:0 -> 7
2:1 0:0 1:0 3:0 4:0 5:0 -> 2
4:1 5:0 -> 1
3:1 4:0 5:0 -> 0
在您的示例掩码中,这无济于事(因为您的掩码设置在位 2 中,因此您必须遍历 bit2==0 和 bit2==1 两侧),但平均而言,它会改善结果(但以设置和更复杂的数据结构为代价)。如果某些位比其他位更有可能被设置,这可能是一个巨大的胜利。如果它们在元素列表中非常接近随机,那么这根本没有帮助。
如果您被基本随机位设置困扰,您应该从后缀树方法中获得大约 (1-5/64)^32
的平均收益(13 倍加速),这可能优于效率差异由于使用了更复杂的操作(但不要指望它——位掩码很快)。如果您的列表中的位是非随机分布的,那么您几乎可以做得很好。
【讨论】:
【参考方案2】:这是按位 Kd 树。每次查找操作通常需要少于 64 次访问。目前,选择枢轴的位(维度)是随机的。
#include <limits.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef unsigned long long Thing;
typedef unsigned long Number;
unsigned thing_ffs(Thing mask);
Thing rand_mask(unsigned bitcnt);
#define WANT_RANDOM 31
#define WANT_BITS 3
#define BITSPERTHING (CHAR_BIT*sizeof(Thing))
#define NONUMBER ((Number)-1)
struct node
Thing value;
Number num;
Number nul;
Number one;
char pivot;
*nodes = NULL;
unsigned nodecount=0;
unsigned itercount=0;
struct node * nodes_read( unsigned *sizp, char *filename);
Number *find_ptr_to_insert(Number *ptr, Thing value, Thing mask);
unsigned grab_matches(Number *result, Number num, Thing mask);
void initialise_stuff(void);
int main (int argc, char **argv)
Thing mask;
Number num;
unsigned idx;
srand (time(NULL));
nodes = nodes_read( &nodecount, argv[1]);
fprintf( stdout, "Nodecount=%u\n", nodecount );
initialise_stuff();
#if WANT_RANDOM
mask = nodes[nodecount/2].value | nodes[nodecount/3].value ;
#else
mask = 0x38;
#endif
fprintf( stdout, "\n#### Search mask=%llx\n", (unsigned long long) mask );
itercount = 0;
num = NONUMBER;
idx = grab_matches(&num,0, mask);
fprintf( stdout, "Itercount=%u\n", itercount );
fprintf(stdout, "KdTree search %16llx\n", (unsigned long long) mask );
fprintf(stdout, "Count=%u Result:\n", idx);
idx = num;
if (idx >= nodecount) idx = nodecount-1;
fprintf( stdout, "num=%4u Value=%16llx\n"
,(unsigned) nodes[idx].num
,(unsigned long long) nodes[idx].value
);
fprintf( stdout, "\nLinear search %16llx\n", (unsigned long long) mask );
for (idx = 0; idx < nodecount; idx++)
if ((nodes[idx].value & mask) == nodes[idx].value) break;
fprintf(stdout, "Cnt=%u\n", idx);
if (idx >= nodecount) idx = nodecount-1;
fprintf(stdout, "Num=%4u Value=%16llx\n"
, (unsigned) nodes[idx].num
, (unsigned long long) nodes[idx].value );
return 0;
void initialise_stuff(void)
unsigned num;
Number root, *ptr;
root = 0;
for (num=0; num < nodecount; num++)
nodes[num].num = num;
nodes[num].one = NONUMBER;
nodes[num].nul = NONUMBER;
nodes[num].pivot = -1;
nodes[num-1].value = 0; /* last node is guaranteed to match anything */
root = 0;
for (num=1; num < nodecount; num++)
ptr = find_ptr_to_insert (&root, nodes[num].value, 0ull );
if (*ptr == NONUMBER) *ptr = num;
else fprintf(stderr, "Found %u for %u\n"
, (unsigned)*ptr, (unsigned) num );
Thing rand_mask(unsigned bitcnt)
struct node * nodes_read( unsigned *sizp, char *filename)
struct node *ptr;
unsigned size,used;
FILE *fp;
if (!filename)
size = (WANT_RANDOM+0) ? WANT_RANDOM : 9;
ptr = malloc (size * sizeof *ptr);
#if (!WANT_RANDOM)
ptr[0].value = 0x0c;
ptr[1].value = 0x0a;
ptr[2].value = 0x08;
ptr[3].value = 0x04;
ptr[4].value = 0x02;
ptr[5].value = 0x01;
ptr[6].value = 0x10;
ptr[7].value = 0x20;
ptr[8].value = 0x00;
#else
for (used=0; used < size; used++)
ptr[used].value = rand_mask(WANT_BITS);
#endif /* WANT_RANDOM */
*sizp = size;
return ptr;
fp = fopen( filename, "r" );
if (!fp) return NULL;
fscanf(fp,"%u\n", &size );
fprintf(stderr, "Size=%u\n", size);
ptr = malloc (size * sizeof *ptr);
for (used = 0; used < size; used++)
fscanf(fp,"%llu\n", &ptr[used].value );
fclose( fp );
*sizp = used;
return ptr;
Thing value = 0;
unsigned bit, cnt;
for (cnt=0; cnt < bitcnt; cnt++)
bit = 54321*rand();
bit %= BITSPERTHING;
value |= 1ull << bit;
return value;
Number *find_ptr_to_insert(Number *ptr, Thing value, Thing done)
Number num=NONUMBER;
while ( *ptr != NONUMBER)
Thing wrong;
num = *ptr;
wrong = (nodes[num].value ^ value) & ~done;
if (nodes[num].pivot < 0) /* This node is terminal */
/* choose one of the wrong bits for a pivot .
** For this bit (nodevalue==1 && searchmask==0 )
*/
if (!wrong) wrong = ~done ;
nodes[num].pivot = thing_ffs( wrong );
ptr = (wrong & 1ull << nodes[num].pivot) ? &nodes[num].nul : &nodes[num].one;
/* Once this bit has been tested, it can be masked off. */
done |= 1ull << nodes[num].pivot ;
return ptr;
unsigned grab_matches(Number *result, Number num, Thing mask)
Thing wrong;
unsigned count;
for (count=0; num < *result; )
itercount++;
wrong = nodes[num].value & ~mask;
if (!wrong) /* we have a match */
if (num < *result) *result = num; count++;
/* This is cheap pruning: the break will omit both subtrees from the results.
** But because we already have a result, and the subtrees have higher numbers
** than our current num, we can ignore them. */
break;
if (nodes[num].pivot < 0) /* This node is terminal */
break;
if (mask & 1ull << nodes[num].pivot)
/* avoid recursion if there is only one non-empty subtree */
if (nodes[num].nul >= *result) num = nodes[num].one; continue;
if (nodes[num].one >= *result) num = nodes[num].nul; continue;
count += grab_matches(result, nodes[num].nul, mask);
count += grab_matches(result, nodes[num].one, mask);
break;
mask |= 1ull << nodes[num].pivot;
num = (wrong & 1ull << nodes[num].pivot) ? nodes[num].nul : nodes[num].one;
return count;
unsigned thing_ffs(Thing mask)
unsigned bit;
#if 1
if (!mask) return (unsigned)-1;
for ( bit=random() % BITSPERTHING; 1 ; bit += 5, bit %= BITSPERTHING)
if (mask & 1ull << bit ) return bit;
#elif 0
for (bit =0; bit < BITSPERTHING; bit++ )
if (mask & 1ull <<bit) return bit;
#else
mask &= (mask-1); // Kernighan-trick
for (bit =0; bit < BITSPERTHING; bit++ )
mask >>=1;
if (!mask) return bit;
#endif
return 0xffffffff;
struct node * nodes_read( unsigned *sizp, char *filename)
struct node *ptr;
unsigned size,used;
FILE *fp;
if (!filename)
size = (WANT_RANDOM+0) ? WANT_RANDOM : 9;
ptr = malloc (size * sizeof *ptr);
#if (!WANT_RANDOM)
ptr[0].value = 0x0c;
ptr[1].value = 0x0a;
ptr[2].value = 0x08;
ptr[3].value = 0x04;
ptr[4].value = 0x02;
ptr[5].value = 0x01;
ptr[6].value = 0x10;
ptr[7].value = 0x20;
ptr[8].value = 0x00;
#else
for (used=0; used < size; used++)
ptr[used].value = rand_mask(WANT_BITS);
#endif /* WANT_RANDOM */
*sizp = size;
return ptr;
fp = fopen( filename, "r" );
if (!fp) return NULL;
fscanf(fp,"%u\n", &size );
fprintf(stderr, "Size=%u\n", size);
ptr = malloc (size * sizeof *ptr);
for (used = 0; used < size; used++)
fscanf(fp,"%llu\n", &ptr[used].value );
fclose( fp );
*sizp = used;
return ptr;
更新:
我对枢轴选择进行了一些实验,偏爱具有最高区分值(“信息内容”)的位。这涉及:
制作位使用的直方图(可以在初始化时完成) 在构建树时:选择频率最接近其余子树中 1/2 的那棵。结果:随机枢轴选择表现更好。
【讨论】:
你能解释一下这个算法是如何工作的吗?我不确定仅阅读代码就可以猜到这一点。在这段代码中插入几个打印,我注意到“树”实际上是一个线性链。而且grab_matches
永远不会被递归调用...
它试图避免递归。你使用了真实的测试数据吗?顺便说一句,在发布之前,我剥离了所有调试-fprintf()s ;-) 该算法与上面的 Elkamina 非常相似。但它从节点和 mask_at_test 之间不同的一组位中选择它的枢轴位(即 XOR 和掩码位)。实际上是一个 Kd 树,具有一位宽的维度。顺便说一句:你是使用文件中的真实测试数据,还是只使用内置的?
OP 发布了一些测试数据。链接在 OP 下的 cmets 中。编辑:不在 cmets 中,而是在 OP 中。
抱歉,没注意到。很少会重复 1 或 2 次。
在大多数情况下,只有一个孩子。只有在有两个时才会递归。 (如果您将 nul,one 成员重命名为 prev,next 可能会有所帮助;-)【参考方案3】:
如下构造一棵二叉树:
-
每一级对应一个位
对应的位在右边,否则在左边
这样在数据库中插入每个数字。
现在,对于搜索:如果掩码中的对应位为 1,则遍历两个孩子。如果为0,则只遍历左节点。基本上一直遍历树,直到你碰到叶子节点(顺便说一句,0 是每个掩码的命中!)。
这棵树将需要 O(N) 空间。
例如 1 (001)、2(010) 和 5 (101) 的树
root
/ \
0 1
/ \ |
0 1 0
| | |
1 0 1
(1) (2) (5)
【讨论】:
这真的比线性搜索好吗?在列表中找到 first 元素的要求呢? @maxtaldykin 这比线性搜索要好得多。不过,我不确定第一个元素的要求。我认为这些数字已排序(在这种情况下我的算法有效)。 @ElKamina,要找到与您需要在叶子中存储索引的最小索引匹配。找到第一个匹配项后,您需要继续遍历树以找到最小索引。它似乎并不比 O(N) 快多少。【参考方案4】:使用预先计算的位掩码。形式上仍然是 O(N),因为与掩码操作是 O(N)。最后的 pass 也是 O(N),因为它需要找到最低位集合,但这也可以加快。
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* For demonstration purposes.
** In reality, this should be an unsigned long long */
typedef unsigned char Thing;
#define BITSPERTHING (CHAR_BIT*sizeof (Thing))
#define COUNTOF(a) (sizeof a / sizeof a[0])
Thing data[] =
/****** index abcdef */
0x0c /* 0 001100 */
, 0x0a /* 1 001010 */
, 0x08 /* 2 001000 */
, 0x04 /* 3 000100 */
, 0x02 /* 4 000010 */
, 0x01 /* 5 000001 */
, 0x10 /* 6 010000 */
, 0x20 /* 7 100000 */
, 0x00 /* 8 000000 */
;
/* Note: this is for demonstration purposes.
** Normally, one should choose a machine wide unsigned int
** for bitmask arrays.
*/
struct bitmap
char data[ 1+COUNTOF (data)/ CHAR_BIT ];
nulmaps [ BITSPERTHING ];
#define BITSET(a,i) (a)[(i) / CHAR_BIT ] |= (1u << ((i)%CHAR_BIT) )
#define BITTEST(a,i) ((a)[(i) / CHAR_BIT ] & (1u << ((i)%CHAR_BIT) ))
void init_tabs(void);
void map_empty(struct bitmap *dst);
void map_full(struct bitmap *dst);
void map_and2(struct bitmap *dst, struct bitmap *src);
int main (void)
Thing mask;
struct bitmap result;
unsigned ibit;
mask = 0x38;
init_tabs();
map_full(&result);
for (ibit = 0; ibit < BITSPERTHING; ibit++)
/* bit in mask is 1, so bit at this position is in fact a don't care */
if (mask & (1u <<ibit)) continue;
/* bit in mask is 0, so we can only select items with a 0 at this bitpos */
map_and2(&result, &nulmaps[ibit] );
/* This is not the fastest way to find the lowest 1 bit */
for (ibit = 0; ibit < COUNTOF (data); ibit++)
if (!BITTEST(result.data, ibit) ) continue;
fprintf(stdout, " %u", ibit);
fprintf( stdout, "\n" );
return 0;
void init_tabs(void)
unsigned ibit, ithing;
/* 1 bits in data that dont overlap with 1 bits in the searchmask are showstoppers.
** So, for each bitpos, we precompute a bitmask of all *entrynumbers* from data[], that contain 0 in bitpos.
*/
memset(nulmaps, 0 , sizeof nulmaps);
for (ithing=0; ithing < COUNTOF(data); ithing++)
for (ibit=0; ibit < BITSPERTHING; ibit++)
if ( data[ithing] & (1u << ibit) ) continue;
BITSET(nulmaps[ibit].data, ithing);
/* Logical And of two bitmask arrays; simular to dst &= src */
void map_and2(struct bitmap *dst, struct bitmap *src)
unsigned idx;
for (idx = 0; idx < COUNTOF(dst->data); idx++)
dst->data[idx] &= src->data[idx] ;
void map_empty(struct bitmap *dst)
memset(dst->data, 0 , sizeof dst->data);
void map_full(struct bitmap *dst)
unsigned idx;
/* NOTE this loop sets too many bits to the left of COUNTOF(data) */
for (idx = 0; idx < COUNTOF(dst->data); idx++)
dst->data[idx] = ~0;
【讨论】:
因为掩码是drawn uniformly from the space of 64-bit integers
,所以这种方法平均应该可以提高2倍的速度。
可以通过一次为多个位创建预计算位掩码来进一步减少位操作的数量,但会消耗更多内存,例如 4 位半字节:16*16 位掩码,每个位掩码覆盖 1/ 64 位字中的 16 个(4 位)。位掩码也会变得不那么密集,可以通过为集合选择不同的表示来利用这一点。它可能比简单的 seq 搜索要快,但在形式上仍然是 O(N)。
该算法保留了简单线性搜索的所有优点。它简单、线性、可预测、可向量化和缓存友好。至于进一步的优化,4bit nibbles 看起来不太有希望。我相信,使用所有可能的 2 位排列将使速度再提高 2 倍,只占用 30 倍以上的内存(可能仍在缓存中)。从理论上讲,所有 3,4 位排列的速度提高了 4 倍,但有限的内存带宽会破坏它。我想知道是否有可能发明更好的位组合...
我的想法也是从两位半字节开始的。在任何情况下,生成的位集都是相同的大小(N 位),只是更稀疏。利用稀疏可能会节省一些内存和带宽。成对的 and-ing 可以并行化,是的。
顺便说一句:我正在使用 K-d 树寻找更好的解决方案。很难,因为掩码起到通配符的作用。看起来仍然很有希望。以上是关于有效地找到匹配位掩码的第一个元素的主要内容,如果未能解决你的问题,请参考以下文章