计算字符串的所有 1-hamming 距离邻居的最快方法?
Posted
技术标签:
【中文标题】计算字符串的所有 1-hamming 距离邻居的最快方法?【英文标题】:Fastest way to compute all 1-hamming distanced neighbors of strings? 【发布时间】:2015-04-09 15:45:29 【问题描述】:我正在尝试计算 n 个节点图中每个节点之间的汉明距离。此图中的每个节点都有一个相同长度 (k) 的标签,用于标签的字母表是 0, 1, *。 '*' 用作无关符号。例如,标签 101*01 和 1001*1 之间的汉明距离等于 1(我们说它们仅在第 3 个索引处不同)。
我需要做的是找到每个节点的所有 1 汉明距离邻居,并准确报告这两个标签在哪个索引处不同。
我将每个节点标签与所有其他节点标签逐个字符进行比较,如下所示:
// Given two strings s1, s2
// returns the index of the change if hd(s1,s2)=1, -1 otherwise.
int count = 0;
char c1, c2;
int index = -1;
for (int i = 0; i < k; i++)
// do not compute anything for *
c1 = s1.charAt(i);
if (c1 == '*')
continue;
c2 = s2.charAt(i);
if (c2 == '*')
continue;
if (c1 != c2)
index = i;
count++;
// if hamming distance is greater than 1, immediately stop
if (count > 1)
index = -1;
break;
return index;
我可能有几百万个节点。 k 通常在 50 左右。我使用的是 JAVA,这种比较需要 n*n*k 时间并且运行缓慢。我考虑使用尝试和 VP 树,但无法弄清楚哪种数据结构适用于这种情况。我还研究了 Simmetrics 库,但什么都没有出现在我的脑海中。如果有任何建议,我将不胜感激。
【问题讨论】:
【参考方案1】:试试这个方法:
将键转换为三进制数(以 3 为底)。即 0=0, 1=1, *=2 10 位三进制为您提供 0..59049 的范围,适合 16 位。
这意味着其中两个将形成一个 32 位字。创建一个包含 40 亿个条目的查找表,返回这两个 10 位三元词之间的距离。
您现在可以使用查找表通过一次查找来检查密钥的 10 个字符。如果您使用 5 个字符,那么 3^5 会为您提供 243 个值,这些值适合一个字节,因此查找表只有 64 KB。
通过使用移位操作,您可以创建不同大小的查找表来平衡内存和速度。
这样,您可以优化循环以更快地中止。
要获得第一个差异的位置,您可以使用第二个查找表,其中包含两个关键子字符串的第一个差异的索引。
如果您有数百万个节点,那么您将有许多以相同的子字符串开头。尝试将它们分类到存储桶中,其中一个存储桶包含以相同键开头的节点。这里的目标是使桶尽可能小(以减少 n*n)。
【讨论】:
感谢您的回答。我正在考虑如何使用查找表。只有我预先计算它才有优势。因为我只需要一次距离。但是,字符串大小 (k) 不是一个固定数字。对于我的测试,它的值在 4 到 75 之间。我不知道如何确定大小。此外,桶排序还具有 n*n 的最坏情况性能。我错过了一点吗? 您可以在末尾用0
填充较短的键;使第一个变化的距离和索引保持不变。对于较长的键,您需要将它们拆分并进行多次查找。
对于桶排序,可以使用哈希映射,其中key是节点key的长度(如果两个节点key的长度相差超过1位,那么它们必须有一个汉明距离大于 2)。在这些存储桶中,您可以按密钥的前 M 位数字进行排序/散列。这应该会给你 O(N) 的桶准备时间。
我尝试使用查找表,但由于内存限制,我能够为 5 位标签创建查找表。不幸的是,这个操作似乎比前一个慢了 2-3 倍(不包括创建查找表的时间)。
尝试只转换一次节点键。运行分析器以查看时间花在了哪里。【参考方案2】:
存储 1 位的掩码和 * 位的掩码,而不是 / 附加到字符串。可以使用 BitSet,但让我们尝试不使用。
static int mask(String value, char digit)
int mask = 0;
int bit = 2; // Start with bits[1] as per specification.
for (int i = 0; i < value.length(); ++i)
if (value.charAt(i) == digit)
mask |= bit;
bit <<= 1;
return mask;
class Cell
int ones;
int stars;
int difference(Cell x, Cell y)
int distance = 0;
return (x.ones & ~y.stars) ^ (y.ones & ~x.stars);
int hammingDistance(Cell x, Cell y)
return Integer.bitCount(difference(x, y));
boolean differsBy1(Cell x, Cell y)
int diff = difference(x, y);
return diff == 0 ? false : (diff & (diff - 1)) == 0;
int bitPosition(int diff)
return Integer.numberOfTrailingZeroes(diff);
【讨论】:
绝对同意这种(位掩码)方法,但有两个小问题:(1)我看不到“规范 [to] Start with bits[1]”,我认为位置 [0] 在字符串自然映射到值为 1 的 1int(32 位)但确实适合long
(64 位)。如果 k 是 65..128,则使用两个 long
并且 'differsBy1` 会变得稍微复杂一些 hidiff==0 && lodiff!=0 && (lodiff&(lodiff-1))==0 || hidiff!=0 && (hidiff&(hidiff-1))==0 && lodiff==0
。
@dave_thompson_085 感谢您投入这么多心思。【参考方案3】:
有趣的问题。没有通配符符号会很容易。
如果通配符是字母表中的常规字符,那么对于给定的字符串,您可以枚举所有 k 汉明距离为 1 的字符串。然后在多映射中查找这些字符串。例如,对于 101,您查找 001,111 和 100。
不关心符号使您无法进行该查找。但是,如果构建多映射以使每个节点都由其所有可能的键存储,则您可以再次进行该查找。例如,1*1 存储为 111 和 101。因此,当您查找 10* 时,您会查找 000,010,011,001,111,它会找到由 111 存储的 1*1。
这样做的好处是您可以将所有标签存储为整数而不是三元结构,因此以 int[3] 作为键值,您可以使用任何 k
性能取决于多地图的支持实现。理想情况下,您会为密钥大小 O(n*k*log(n)) 中的距离为 1 的邻居。构建多映射需要O(n * 2 ^ z)
,其中z
是任何字符串的最大通配符数。如果通配符的平均数量很少,这应该是可以接受的性能损失。
编辑:您将所有节点的查找性能提高到O(n*log(n))
,方法是将汉明距离为 1 的邻居也插入到多地图中,但这可能会扩大其大小。
注意:我是在午休时间输入的。我还没有检查详细信息。
【讨论】:
以上是关于计算字符串的所有 1-hamming 距离邻居的最快方法?的主要内容,如果未能解决你的问题,请参考以下文章