找到任何子串的最小汉明距离的最快方法?

时间:2009-07-17 22:49:22

标签: performance algorithm string

给定一个长字符串L和一个较短的字符串S(约束是L。长度必须是> = S。length),我想要找到SL的任何子字符串之间的最小汉明距离,其长度等于S。length。让我们调用此minHamming()的函数。例如,

minHamming(ABCDEFGHIJ, CDEFGG) == 1

minHamming(ABCDEFGHIJ, BCDGHI) == 3

以明显的方式(枚举L的每个子字符串)执行此操作需要O(S。length * L。length)时间。在次线性时间有没有聪明的方法可以做到这一点?我使用多个不同的L字符串搜索相同的S,因此对L进行一次复杂的预处理是可以接受的。

编辑:修改后的Boyer-Moore是个好主意,除了我的字母只有4个字母(DNA)。

3 个答案:

答案 0 :(得分:15)

也许令人惊讶的是,使用快速傅立叶变换(FFT)可以在O(| A | nlog n)时间中解决这个确切的问题,其中n是较大序列{{1}的长度}和L是字母表的大小。

以下是Donald Benson撰写的一篇免费的PDF文件,描述了它的工作原理:

摘要:将每个字符串|A|S转换为多个指标向量(每个字符一个,所以4个DNA),然后convolve相应的向量,以确定每个可能的对齐的匹配计数。诀窍在于“时间”域中的卷积,通常需要O(n ^ 2)时间,可以使用“频率”域中的乘法来实现,这只需要O(n)时间,加上转换所需的时间在域之间再回来。使用FFT,每次转换只需要O(nlog n)时间,因此总时间复杂度为O(| A | nlog n)。为了获得最大速度,使用有限域 FFT,它只需要整数运算。

注意:对于任意LS,此算法显然是对L和{{|S|和{{简单的O(mn)算法的巨大胜利1}}变大,但OTOH如果|L|通常比S短(例如查询带有小序列的大型DB),那么显然这种方法不会加速。

UPDATE 21/7/2009 :更新后提及时间复杂度也线性地取决于字母表的大小,因为字母表中的每个字符都必须使用一对单独的指示符向量

答案 1 :(得分:2)

修改过的Boyer-Moore

我刚刚挖出了Boyer-Moore的一些旧的Python实现,我已经躺在那里并修改了匹配循环(将文本与模式进行比较)。一旦在两个字符串之间发现第一个不匹配,就不会爆发,只需计算不匹配的数量,但记住第一个不匹配

current_dist = 0
while pattern_pos >= 0:
    if pattern[pattern_pos] != text[text_pos]:
        if first_mismatch == -1:
            first_mismatch = pattern_pos
            tp = text_pos
        current_dist += 1
        if current_dist == smallest_dist:
           break

     pattern_pos -= 1
     text_pos -= 1

 smallest_dist = min(current_dist, smallest_dist)
 # if the distance is 0, we've had a match and can quit
 if current_dist == 0:
     return 0
 else: # shift
     pattern_pos = first_mismatch
     text_pos = tp
     ...

如果此时字符串不完全匹配,请通过恢复值返回到第一个不匹配点。这可以确保实际找到最小距离。

整个实现相当长(~150LOC),但我可以根据要求发布。上面概述了核心思想,其他一切都是标准的Boyer-Moore。

对文本进行预处理

另一种加快速度的方法是预处理文本以获得字符位置的索引。您只想在两个字符串之间至少出现一个匹配的位置开始比较,否则汉明距离为| S |平凡。

import sys
from collections import defaultdict
import bisect

def char_positions(t):
    pos = defaultdict(list)
    for idx, c in enumerate(t):
        pos[c].append(idx)
    return dict(pos)

此方法只创建一个字典,将字体中的每个字符映射到其出现的排序列表。

比较循环或多或少没有改变到幼稚的O(mn)方法,除了我们不增加每次开始比较1的位置,而是基于角色位置:

def min_hamming(text, pattern):
    best = len(pattern)
    pos = char_positions(text)

    i = find_next_pos(pattern, pos, 0)

    while i < len(text) - len(pattern):
        dist = 0
        for c in range(len(pattern)):
            if text[i+c] != pattern[c]:
                dist += 1
                if dist == best:
                    break
            c += 1
        else:
            if dist == 0:
                return 0
        best = min(dist, best)
        i = find_next_pos(pattern, pos, i + 1)

    return best

实际改进在find_next_pos

def find_next_pos(pattern, pos, i):
    smallest = sys.maxint
    for idx, c in enumerate(pattern):
        if c in pos:
            x = bisect.bisect_left(pos[c], i + idx)
            if x < len(pos[c]):
                smallest = min(smallest, pos[c][x] - idx)
    return smallest

对于每个新位置,我们找到S中字符出现在L的最低索引。如果没有这样的索引,算法将终止。

find_next_pos当然很复杂,人们可以尝试通过仅使用模式S的前几个字符来改进它,或者使用一个集合来确保模式中的字符不会被检查两次。

讨论

哪种方法更快,很大程度上取决于您的数据集。你的字母表越多样化,跳跃就越大。如果你有一个很长的L,那么预处理的第二种方法可能会更快。对于非常非常短的字符串(如你的问题),天真的方法肯定是最快的。

DNA

如果你的字母非常小,你可以尝试获得角色双字母(或更大)的角色位置,而不是unigrams。

答案 2 :(得分:-1)

就像大O一样,你陷入困境。从根本上说,你需要测试目标中的每个字母是否与子字符串中的每个符合条件的字母匹配。

幸运的是,这很容易并行化。

您可以应用的一个优化是保持当前位置的不匹配运行计数。如果它到目前为止大于最低汉明距离,那么显然你可以跳到下一个可能性。