问题:
给定一个大的(~1亿)无符号32位整数列表,无符号32位整数输入值和最大Hamming Distance,返回指定汉明距离内的所有列表成员。输入值。
保持列表的实际数据结构是开放的,性能要求决定了内存解决方案,构建数据结构的成本是次要的,查询数据结构的低成本是至关重要的。
示例:
For a maximum Hamming Distance of 1 (values typically will be quite small)
And input:
00001000100000000000000001111101
The values:
01001000100000000000000001111101
00001000100000000010000001111101
should match because there is only 1 position in which the bits are different.
11001000100000000010000001111101
should not match because 3 bit positions are different.
到目前为止我的想法:
对于汉明距离为0的退化情况,只需使用排序列表并对特定输入值进行二分搜索。
如果汉明距离只有1,我可以翻转原始输入中的每一位并重复上述32次。
如何有效地(不扫描整个列表)发现具有汉明距离的列表成员> 1。
答案 0 :(得分:100)
问题:我们对汉明距离d(x,y)了解多少?
<强>答案:强>
问题:我们为什么关心?
答案:因为这意味着汉明距离是指标空间的指标 。有用于索引度量空间的算法。
您通常也可以查找“空间索引”的算法,并且知道您的空间不是欧几里德,但是度量空间。关于此主题的许多书籍都使用诸如汉明距离之类的度量标准进行字符串索引。
脚注:如果您要比较固定宽度字符串的汉明距离,则可以通过使用汇编或处理器内在函数来显着提高性能。例如,使用GCC(manual)执行此操作:
static inline int distance(unsigned x, unsigned y)
{
return __builtin_popcount(x^y);
}
如果你然后通知GCC你正在为一台装有SSE4a的计算机进行编译,那么我认为应该减少到只有几个操作码。
编辑:根据许多消息来源,这有时/通常比通常的掩码/移位/添加代码慢。基准测试显示,在我的系统上,C版本的GCC __builtin_popcount
的表现优于约160%。
附录:我自己对这个问题感到好奇,所以我分析了三个实现:线性搜索,BK树和VP树。请注意,VP和BK树非常相似。 BK树中节点的子节点是树的“壳”,其中包含距离树中心固定距离的点。 VP树中的节点有两个子节点,一个包含以节点中心为中心的球体内的所有点,另一个包含外部所有点的子节点。因此,您可以将VP节点视为具有两个非常厚的“壳”的BK节点,而不是许多更精细的“壳”。
结果是在我的3.2 GHz PC上捕获的,算法不会尝试使用多个核心(应该很容易)。我选择了100M伪随机整数的数据库大小。结果是距离1..5的1000个查询的平均值,以及6..10的100个查询和线性搜索的平均值。
-- BK Tree -- -- VP Tree -- -- Linear -- Dist Results Speed Cov Speed Cov Speed Cov 1 0.90 3800 0.048% 4200 0.048% 2 11 300 0.68% 330 0.65% 3 130 56 3.8% 63 3.4% 4 970 18 12% 22 10% 5 5700 8.5 26% 10 22% 6 2.6e4 5.2 42% 6.0 37% 7 1.1e5 3.7 60% 4.1 54% 8 3.5e5 3.0 74% 3.2 70% 9 1.0e6 2.6 85% 2.7 82% 10 2.5e6 2.3 91% 2.4 90% any 2.2 100%
在你的评论中,你提到了:
我认为BK树可以通过生成一堆具有不同根节点的BK树并将它们展开来改进。
我认为这正是VP树比BK树(稍微)表现更好的原因。 “更深”而不是“浅”,它与更多点进行比较,而不是使用更细粒度的比较来减少点数。我怀疑高维空间的差异更为极端。
最后一个提示:树中的叶节点应该是用于线性扫描的平面整数数组。对于小集(可能是1000点或更少),这将更快,更高效。
答案 1 :(得分:8)
我写了一个解决方案,其中我在2个 32 位的位集中表示输入数字,因此我可以在O(1)中检查输入中是否存在某个数字。然后,对于查询的数字和最大距离,我递归地生成该距离内的所有数字,并根据该位集检查它们。
例如,对于最大距离5,这是242825个数字(sumd = 0 to 5 {32 choose d})。相比之下,Dietrich Epp的VP树解决方案例如在1亿个数字中占22%,即通过2200万个数字。
我使用Dietrich的代码/解决方案作为添加我的解决方案并与之比较的基础。以下是速度,以每秒查询数为单位,最大距离为10:
Dist BK Tree VP Tree Bitset Linear
1 10,133.83 15,773.69 1,905,202.76 4.73
2 677.78 1,006.95 218,624.08 4.70
3 113.14 173.15 27,022.32 4.76
4 34.06 54.13 4,239.28 4.75
5 15.21 23.81 932.18 4.79
6 8.96 13.23 236.09 4.78
7 6.52 8.37 69.18 4.77
8 5.11 6.15 23.76 4.68
9 4.39 4.83 9.01 4.47
10 3.69 3.94 2.82 4.13
Prepare 4.1s 21.0s 1.52s 0.13s
times (for building the data structure before the queries)
对于小距离,bitset解决方案是迄今为止最快的解决方案。问题作者埃里克在下面评论说,最大的兴趣距离可能是4-5。当然,我的bitset解决方案对于较大的距离变得更慢,甚至比线性搜索更慢(对于距离32,它将经历2 32 数字)。但是对于距离9,它仍然很容易引导。
我还修改了迪特里希的测试。以上每个结果都是为了让算法在大约15秒内解决至少三个查询和尽可能多的查询(我会查询1,2,4,8,16等查询,直到至少10秒钟为止总共通过了)。那是相当稳定的,我甚至得到相似的数字只有1秒钟。
我的CPU是i7-6700。 My code (based on Dietrich's) is here(至少暂时忽略那里的文档,不知道如何处理,但tree.c
包含所有代码,test.bat
显示我编译和运行的方式(我使用过)迪特里希的旗帜Makefile
))。 Shortcut to my solution
一个警告:我的查询结果只包含一次数字,因此如果输入列表包含重复的数字,则可能需要也可能不需要。在提问作者埃里克的案例中,没有重复(见下面的评论)。在任何情况下,这个解决方案可能对那些在输入中没有重复或者不想在查询结果中需要或需要重复的人来说是好的(我认为纯查询结果可能只是一个意味着结束,然后一些其他代码将数字转换为其他代码,例如将数字映射到散列为该数字的文件列表。
答案 2 :(得分:1)
如何对列表进行排序,然后在该排序列表中对汉明距离内的不同可能值进行二分搜索?
答案 3 :(得分:1)
您可以在指定的汉明距离内预先计算原始列表的每个可能变体,并将其存储在布隆过滤器中。这给你一个快速的“否”但不一定是关于“是”的明确答案。
对于YES,存储与bloom过滤器中每个位置关联的所有原始值的列表,并逐个浏览它们。优化布隆过滤器的大小以进行速度/内存权衡。
不确定这一切是否完全正常,但如果你有运行时RAM可以燃烧并且愿意花费很长时间进行预计算,那么这似乎是一个很好的方法。
答案 4 :(得分:1)
解决此问题的一种可能方法是使用Disjoint-set data structure。该想法是在同一组中具有汉明距离&lt; = k的合并列表成员。以下是算法的概要:
对于每个列表成员,使用汉明距离&lt; = k计算每个可能的值。对于k = 1,有32个值(对于32位值)。对于k = 2,32 + 32 * 31/2值。
对于每个计算出的值,请测试它是否在原始输入中。您可以使用大小为2 ^ 32的数组或哈希映射来进行此检查。
如果值位于原始输入中,请使用列表成员执行“联合”操作。
使用N个不相交集启动算法(其中N是输入中的元素数)。每次执行并集操作时,都会将不相交集的数量减少1。当算法终止时,不相交集数据结构将具有以不相交集合分组的汉明距离&lt; = k的所有值。可以在almost linear time中计算这种不相交的数据结构。
答案 5 :(得分:1)
这是一个简单的想法:对100m个输入整数进行字节基数排序,最高有效字节在前,在某些外部结构中跟踪存储桶边界的前三个级别。
要进行查询,请从d
的距离预算和您输入的单词w
开始。对于顶层中每个字节值为b
的存储桶,请计算d_0
与b
高字节之间的汉明距离w
。以d - d_0
的预算递归搜索该存储桶:即,对于每个字节值b'
,令d_1
为b'
与{{的第二个字节之间的汉明距离1}}。以w
的预算递归搜索到第三层,依此类推。
请注意,存储桶形成一棵树。每当您的预算为负数时,就停止搜索该子树。如果您递归地进入一个叶子而没有浪费您的距离预算,则该叶子值应该是输出的一部分。
这里是表示外部存储桶边界结构的一种方法:具有长度为16_777_216(d - d_0 - d_1
)的数组,其中索引= (2**8)**3 = 2**24
的元素是存储桶中包含范围[ 256 * i,256 * i + 255]。要在该存储桶的末尾找到索引1,请查找索引i + 1(或将数组的末尾用于i + 1 = 2 ** 24)。
内存预算为100m *每个单词4个字节= 400 MB用于输入,而2 ** 24 * 4个字节每个地址= 64 MiB用于索引结构,或总计略少于半个千兆字节。索引结构在原始数据上的开销为6.25%。当然,一旦构建了索引结构,您只需要存储每个输入字的最低字节,因为其他三个在索引结构中的索引中都是隐式的,总共约为(64 + 50)MB。
如果输入不是均匀分布的,则可以使用(单个,通用共享)置换来置换输入单词的位,该置换将所有熵置于树的顶部。这样,第一级修剪将消除较大的搜索空间。
我尝试了一些实验,它的性能与线性搜索一样好,有时甚至更糟。这个奇特的想法非常重要。哦,好吧,至少它的内存效率高。