计算字符串的所有 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 &amp;&amp; lodiff!=0 &amp;&amp; (lodiff&amp;(lodiff-1))==0 || hidiff!=0 &amp;&amp; (hidiff&amp;(hidiff-1))==0 &amp;&amp; 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 距离邻居的最快方法?的主要内容,如果未能解决你的问题,请参考以下文章

高维数据中的最近邻?

用于拟合 scikit 邻居/半径分类的预计算矩阵

最近邻居图中第 k 个邻居的奇怪距离

面试算法: 广度优先搜索 BFS

kNN - 如何根据计算出的距离在训练矩阵中定位最近的邻居

计算 3D 中两条线(线段)之间的最短距离