【问题标题】:Fastest way to find minimal Hamming distance to any substring?找到任何子串的最小汉明距离的最快方法?
【发布时间】:2010-11-11 20:59:00
【问题描述】:

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

minHamming(ABCDEFGHIJ, CDEFGG) == 1.

minHamming(ABCDEFGHIJ, BCDGHI) == 3.

这样做很明显(枚举 L 的每个子字符串)需要 O(S.length * L.length) 时间。有没有什么聪明的方法可以在亚线性时间内做到这一点?我用几个不同的S 字符串搜索相同的L,因此对L 进行一些复杂的预处理是可以接受的。

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

【问题讨论】:

  • 如果 S 比 L 短,你的约束应该是 (L.length >= S.length) 吗?
  • “几个不同”有多少? |L|有多大和|S|,相对/绝对? ~~~~

标签: performance algorithm string


【解决方案1】:

就 big-O 而言,您被卡住了。从根本上讲,您需要测试目标中的每个字母是否与子字符串中的每个符合条件的字母匹配。

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

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

【讨论】:

  • -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

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

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

讨论

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

DNA

如果您的字母表非常小,您可以尝试获取字符二元组(或更大)而不是一元组的字符位置。

【讨论】:

  • 哦,对了,很抱歉所有的代码示例都是用 Python 编写的,但这只是我最熟悉的语言。请随时要求澄清!
  • 就个人而言,我认为人们应该始终以他们最熟悉的任何语言发布代码。如果不出意外,它会教其他人一些关于该语言的知识。
  • +1 用于干净的代码示例,我会再次+1(如果允许)用于带有小字母想法的二元组或更大。毕竟,4 克 DNA 类似于 8 位字节。您只需要考虑一下从 n-gram 转换回真实序列的过程。
【解决方案3】:

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

这是唐纳德·本森 (Donald Benson) 的一篇论文的免费 PDF 文件,描述了它的工作原理:

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

注意:对于任意的SL,这个算法显然比简单的O(mn) 算法有巨大的性能优势,因为|S||L| 变大了,但是OTOH如果S 通常比log|L| 短(例如,在查询具有小序列的大型数据库时),那么显然这种方法不会提供加速。

2009 年 7 月 21 日更新:更新提到时间复杂度还线性地取决于字母表的大小,因为必须为字母表中的每个字符使用一对单独的指示向量.

【讨论】:

  • 不适合我的情况 b/c S 与 L 相比非常小,但无论如何都是一个足够有趣的答案,值得一票。
  • 更新提到时间复杂度也线性地取决于字母表的大小。
  • 谢谢丹尼尔!是的,FFT 对我来说是一种神奇的东西 - 好像你不应该能够快速计算一些东西:) 对我来说,这个应该是不可能的类别中的其他事情是后缀树的事实/arrays 可以在线性时间内构建和查询 :)
  • 整洁! @dsimcha:如果 S 非常小——短至 log|L|信件——那何必呢?请注意,您有一个 Ω(|L|) 下限(您需要查看 L 中的所有字母)...所以您是否处于 |L| 的状态?足够小,但 |L|*log|L|是不是太大了?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-28
  • 2017-04-14
  • 1970-01-01
相关资源
最近更新 更多