【问题标题】:Approximate string matching using backtracking使用回溯的近似字符串匹配
【发布时间】:2011-11-25 07:13:15
【问题描述】:

我想使用回溯来搜索允许可变长度匹配的长字符串中的所有子字符串——即允许最大给定数量的不匹配、插入和删除的匹配。我找不到任何有用的例子。我找到的最接近的是这篇论文here,但这非常复杂。有人吗?

干杯,

马丁

【问题讨论】:

  • 注意。感谢您提供其他建议,但也请给我看一些优雅的回溯代码!

标签: algorithm string-matching backtracking


【解决方案1】:

我将从Levenshtein's distance 算法开始,这是通过不匹配、插入和删除检查字符串相似性时的标准方法。

由于算法使用bottom up dynamic programming,您可能能够找到所有子字符串,而无需为每个潜在子字符串执行算法。

【讨论】:

  • 谢谢,但我对如何为此使用回溯非常感兴趣。
  • 好的,我。但是,如果您对字符串距离感兴趣,我会坚持使用动态编程。如果您对回溯感兴趣,我会找到更合适的问题。只是我的 2 美分...
  • 在生物序列模式匹配方面,scan_for_matches blog.theseed.org/servers/2010/07/scan-for-matches.html 优于所有动态编程软件 AFAIK。 scan_for_matches 基于回溯。
  • 感谢您澄清这一点 - 我不会猜到的。
  • @maasha:在最坏的情况下,基于回溯的近似字符串搜索在模式长度上需要时间指数——例如如果您尝试将模式 "abcd" 与文本 "aabbcce" 匹配最多 3 次插入或删除,则必须一直尝试删除 a、b 和 c 中的每一个的 2^3=8 种方法中的每一种“c != e”比较,即使最终比较意味着最终答案是“不匹配”。在您尝试过的非病理情况下,scan-for-matches 可能会更快,这很好,但在最坏的情况下它比 DP 算法慢。
【解决方案2】:

我所知道的最好的算法是 Gene Myers 的 A Fast Bit-Vector Algorithm for Approximate String Matching Based on Dynamic Programming。给定一个长度为 n 的要搜索的文本、一个长度为 m 的要搜索的模式字符串和最大不匹配/插入/删除数 k,这个算法需要时间 O(mn/w),其中 w 是您计算机的字长(32或 64)。如果您对字符串算法了解很多,那么实际上存在一种算法需要时间与 k 无关——在很长一段时间内,这似乎是一个不可能实现的目标。

我不知道上述算法的现有实现。如果您想要一个工具,agrep 可能正是您所需要的。它使用了一个较早的算法,需要时间 O(mnk/w),但对于低 k 来说已经足够快了——在最坏的情况下,它比回溯搜索提前了几英里。

agrep 基于Shift-Or (or "Bitap") algorithm,这是一种非常聪明的动态规划算法,它设法将其状态表示为整数中的位,并通过二进制加法来完成大部分工作更新状态,这使算法比更典型的实现提高了 32 或 64 倍。 :) Myers 的算法也使用这个想法来获得它的 1/w 速度因子。

【讨论】:

  • 我针对 scan_for_mathches 测试了 nrgrep(部分是 bitap 的实现),并且 scan_for_matches 获胜(请参阅其他评论)。此外,不同的实现(agrep、nrgrep 等)不允许您指定错误、插入和删除的数量,而是指定编辑距离。
  • 那篇 Gene Myers 的论文也非常复杂。我真的需要看一个演练示例和一些简单的原型代码。
【解决方案3】:

算法

下面的函数ff() 使用递归(即回溯)来解决您的问题。基本思想是,在对f() 的任何调用开始时,我们试图将原始“needle”字符串的后缀t 与“haystack”字符串的后缀s 匹配,同时只允许一个每种编辑操作的一定数量。

// ss is the start of the haystack, used only for reporting the match endpoints.
void f(char* ss, char* s, char* t, int mm, int ins, int del) {
    while (*s && *s == *t) ++s, ++t;    // OK to always match longest segment
    if (!*t) printf("%d\n", s - ss);    // Matched; print endpoint of match
    if (mm && *s && *t) f(ss, s + 1, t + 1, mm - 1, ins, del);
    if (ins && *s) f(ss, s + 1, t, mm, ins - 1, del);
    if (del && *t) f(ss, s, t + 1, mm, ins, del - 1);
}

// Find all occurrences of t starting at any position in s, with at most
// mm mismatches, ins insertions and del deletions.
void ff(char* s, char* t, int mm, int ins, int del) {
    for (char* ss = s; *s; ++s) {
//      printf("Starting from offset %d...\n", s - ss);
        f(ss, s, t, mm, ins, del);
    }
}

调用示例:

ff("xxabcydef", "abcdefg", 1, 1, 1);

这个输出:

9
9

因为有两种方法可以在“xxabcydef”中找到“abcdefg”,每种变化最多1个,并且这两种方法都在第9位结束:

Haystack: xxabcydef-
Needle:     abc-defg

其中有 1 个插入(y)和 1 个删除(g),并且

Haystack: xxabcyde-f
Needle:     abc-defg

其中有 1 次插入(y)、1 次删除(f)和 1 次将 g 替换为 f

支配关系

为什么在第 3 行使用 while 循环在两个字符串的开头贪婪地匹配尽可能多的字符实际上是安全的,这可能并不明显。事实上,这可能会减少将特定结束位置报告为匹配的,但它永远不会导致结束位置被完全遗忘——因为我们通常对只是在大海捞针的给定位置是否有匹配结束,如果没有这个while 循环,算法将总是花费时间成指数的针大小,这似乎是双赢的。

由于支配关系,它可以保证工作。为了看到这一点,假设相反——它实际上是不安全的(即错过了一些匹配)。然后会有一些匹配,其中两个字符串中相等字符的初始段彼此不对齐,例如:

Haystack: abbbbc
Needle:   a-b-bc

但是,任何这样的匹配都可以通过将最左边的字符分流到间隙左侧的间隙后,转换为具有相同数量的每种类型的操作并在相同位置结束的另一个匹配:

Haystack: abbbbc
Needle:   ab--bc

如果您反复执行此操作,直到不再可能在不需要替换的情况下分流字符,您将获得一个匹配,其中两个字符串中相同字符的最大初始段相互对齐:

Haystack: abbbbc
Needle:   abb--c

我的算法会找到所有这样的匹配,因此它不会忽略任何匹配位置。

指数时间

与任何回溯程序一样,此函数将在某些输入上表现出指数级减速。当然,也有可能是在你碰巧使用的输入上,这不会发生,而且它比 DP 算法的特定实现运行得更快。

【讨论】:

  • 不,只是举几个例子,直到它看起来有效。 f() 很简单,应该可以通过考虑 s 和 t 输入的所有可能组合来证明它的工作原理:(a) s 是空的,t 不是; (b) t 是空的,s 不是; (c) 两者都是空的; (d) 两者都不为空,且以 1 个或多个相等字符开头; (e) 两者都不是空的,它们以不同的字符开头。请注意,情况 (d) 通过 while 循环转换为其他情况之一,这进一步简化了事情。
  • 你一直说 scan_for_matches 比其他东西“更快”,但你没有给出你实际测试的例子(needle、haystack、#mm、#ins、#del) - - 您是否意识到速度完全取决于此?
  • 我可以想出一些方法来加快速度,而不会丢失任何“重要”匹配。一个是,如果mm > 0,紧跟在删除之后的插入(反之亦然)总是可以被单个替换替换,因为如果我们稍后在同一匹配中需要替换,我们可以只使用插入+删除组合代替(因为每个“保存”了 1 个,我们仍然至少有一个要“花费”)。这意味着在 mm > 0 时禁止在 del 之后插入 ins,反之亦然。
  • 我们还可以要求任何连续的插入和删除块首先包含(比如说)所有插入——这意味着在删除之后禁止插入,句号。最后,因为ff() 依次从每个位置开始,我们对以一个或多个插入开始的匹配不太感兴趣(因为它们都将由ff() 使用以后调用f() 找到),所以@ 987654349@ 可以带一个额外的参数allow_ins,告诉它是否尝试插入。 ff() 将为此传递 0,而 f() 的所有递归调用将传递 1 为通常的行为。
  • 我很糟糕(再次)-我试图颠倒 ins 和 del 的含义并搞砸了。我已经为所有边缘情况编写了单元测试,看起来不错。此外,我在这里整理了自己的 cmets,因为这不是一个讨论论坛——我建议你也这样做。再次感谢您的帮助!我的实现在这里:code.google.com/p/biopieces/source/browse/trunk/code_ruby/lib/…
猜你喜欢
  • 2011-05-11
  • 2013-07-10
  • 1970-01-01
  • 2023-03-23
  • 2010-09-08
  • 2016-03-28
  • 2013-04-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多