【发布时间】:2011-11-25 07:13:15
【问题描述】:
我想使用回溯来搜索允许可变长度匹配的长字符串中的所有子字符串——即允许最大给定数量的不匹配、插入和删除的匹配。我找不到任何有用的例子。我找到的最接近的是这篇论文here,但这非常复杂。有人吗?
干杯,
马丁
【问题讨论】:
-
注意。感谢您提供其他建议,但也请给我看一些优雅的回溯代码!
标签: algorithm string-matching backtracking
我想使用回溯来搜索允许可变长度匹配的长字符串中的所有子字符串——即允许最大给定数量的不匹配、插入和删除的匹配。我找不到任何有用的例子。我找到的最接近的是这篇论文here,但这非常复杂。有人吗?
干杯,
马丁
【问题讨论】:
标签: algorithm string-matching backtracking
我将从Levenshtein's distance 算法开始,这是通过不匹配、插入和删除检查字符串相似性时的标准方法。
由于算法使用bottom up dynamic programming,您可能能够找到所有子字符串,而无需为每个潜在子字符串执行算法。
【讨论】:
我所知道的最好的算法是 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 速度因子。
【讨论】:
下面的函数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 循环转换为其他情况之一,这进一步简化了事情。
mm > 0,紧跟在删除之后的插入(反之亦然)总是可以被单个替换替换,因为如果我们稍后在同一匹配中需要替换,我们可以只使用插入+删除组合代替(因为每个“保存”了 1 个,我们仍然至少有一个要“花费”)。这意味着在 mm > 0 时禁止在 del 之后插入 ins,反之亦然。
ff() 依次从每个位置开始,我们对以一个或多个插入开始的匹配不太感兴趣(因为它们都将由ff() 使用以后调用f() 找到),所以@ 987654349@ 可以带一个额外的参数allow_ins,告诉它是否尝试插入。 ff() 将为此传递 0,而 f() 的所有递归调用将传递 1 为通常的行为。