使用后缀树进行近似子串匹配

时间:2013-10-14 20:43:00

标签: algorithm string-matching suffix-tree edit-distance

本文讨论了使用suffix tree来改善匹配时间的近似子字符串匹配技术。每个答案都针对不同的算法。

  1. 近似子字符串匹配尝试在字符串 P 中查找子字符串(模式) T ,允许最多 {{1 } 不匹配。
  2. 要了解如何创建后缀树,请点击here。但是,某些算法需要额外的预处理。
  3. 我邀请人们添加新算法(即使它不完整)并改进答案。

2 个答案:

答案 0 :(得分:3)

你做得很好。我对这个算法并不熟悉,但今天已经阅读了论文。你写的所有内容都是正确的。你是对的,解释的某些部分假设很多。

您的问题

  

1.根据后缀树或输入字符串,输出大小是指什么?对于标记为输出的所有状态r,最终输出阶段列出T中所有出现的Key(r)。

输出包括T中P的最大k距离匹配。特别是,您将获得每个的最终索引和长度。很明显这也是O(n)(记住big-O是一个上限),但可能更小。这是一个事实,即在不到O(p)的时间内不可能产生p匹配。其余的时间限制仅涉及模式长度和可用前缀的数量,两者都可以任意小,因此输出大小可以占主导地位。考虑k = 0并且输入是' a'用图案重复n次' a'。

  

2.在算法C中,定义了函数dp(第4页);我不明白我代表什么索引。它没有初始化,也没有增加。

你是对的。这是一个错误。循环索引应为i。那么j呢?这是与动态程序中正在处理的输入字符对应的列的索引。它应该是一个输入参数。

让我们后退一步。第6页的示例表是使用前面给出的公式(1-4)从左到右逐列计算的。这些显示只需要前面的D和L列来获得下一个。函数dp只是从j计算列j-1的想法的实现。 D和L的列j分别称为dl。列j-1 D和L是d'l',函数输入参数。

我建议您完成动态程序,直到您理解它为止。该算法完全是为了避免重复列计算。在这里"重复"意味着"在基本部分"中具有相同的值,因为这一切都很重要。不必要的部分不会影响答案。

未压缩的trie只是以明显的方式扩展的压缩的trie,每个字符有一个边。除了"深度"的概念,这是不重要的。在压缩树中,depth(s)只是字符串的长度 - 他称之为Key(s) - 需要从根节点获取。

算法A

算法A只是一个聪明的缓存方案。

他的所有定理和引理都表明1)我们只需要担心列的必要部分和2)列j的基本部分完全由可行前缀Q_j决定。这是以j结尾的输入的最长后缀,其匹配模式的前缀(在编辑距离k内)。换句话说,Q_j是到目前为止所考虑的输入结尾处的k-edit匹配的最大开始。

这就是算法A的伪代码。

Let r = root of (uncompressed) suffix trie
Set r's cached d,l with formulas at end page 7 (0'th dp table columns)
// Invariant: r contains cached d,l
for each character t_j from input text T in sequence
  Let s = g(r, t_j)  // make the go-to transition from r on t_j
  if visited(s)
    r = s
    while no cached d,l on node r
      r = f(r) // traverse suffix edge
    end while
  else
    Use cached d',l' on r to find new columns (d,l) = dp(d',l')
    Compute |Q_j| = l[h], h = argmax(i).d[i]<=k as in the paper 
    r = s
    while depth(r) != |Q_j|
      mark r visited
      r = f(r)  // traverse suffix edge
    end while
    mark r visited
    set cached d,l on node r
  end if
end for

为简单起见,我省略了输出步骤。

什么是遍历后缀边缘?当我们从一个节点r执行此操作时,其中Key(r)= aX(前导a后跟一些字符串X),我们将使用Key X进入节点。结果:我们存储了对应于可行前缀Q_h的每个列。前缀为Q_h的输入后缀的trie节点。函数f(s)= r是后缀转换函数。

对于它的价值,Wikipedia picture of a suffix tree显示了这一点。例如,如果来自&#34; NA&#34;的节点我们遵循后缀边缘,我们到达节点&#34; A&#34;并从那里到&#34;&#34;。我们总是切断主角。因此,如果我们用Key(s)标记状态s,我们有f(&#34; NA&#34;)=&#34; A&#34;和f(&#34; A&#34;)=&#34;&#34;。 (我不知道为什么他没有在论文中标明这样的状态。这会简化许多解释。)

现在这非常酷,因为我们每个可行前缀只计算一列。但它仍然很昂贵,因为我们正在检查每个角色并可能遍历每个角色的后缀边缘。

算法B

算法B的目的是通过跳过输入来加快速度,只触摸那些可能产生新列的字符,即那些与输入的末端匹配的模式的前一个看不见的可行前缀。

正如您所怀疑的那样,算法的关键是loc功能。粗略地说,这将告诉下一个&#34;可能&#34;输入字符是。该算法有点像A *搜索。我们维护与文件中的集合S_i相对应的最小堆(必须具有删除操作)。 (他称之为字典,但这不是一个非常传统的术语用法。)最小堆包含潜在的&#34;下一个状态&#34;关键的是下一个&#34;可能的角色&#34;如上所述。处理一个字符会产生新条目。我们继续前进,直到堆为空。

你绝对正确,他在这里粗略。定理和引理并没有联系在一起,无法对正确性进行论证。他假设你会重做他的工作。我并不完全相信这种挥手。但似乎有足够的东西去解码&#34;他想到的算法。

另一个核心概念是集合S_i,特别是未被消除的子集。我们将这些未消除的状态保留在最小堆H.

你说这个符号模糊了S_i的意图是对的。当我们从左到右处理输入并到达位置i时,我们已经积累了一组到目前为止看到的可行前缀。每次找到新的一个时,都会计算一个新的dp列。在作者的符号中,这些前缀对于所有h&lt; = i或更正式{Q_h | h&lt; = i}。其中每个都有从根到唯一节点的路径。集合S_i包含我们通过从所有这些节点沿着trie中的前沿获取一步所获得的所有状态。这产生与查看Q_h和下一个字符a的每次出现的整个文本相同的结果,然后将对应于Q_h a的状态添加到S_i中,但是它更快。 S_i状态的密钥恰好是下一个可行前缀Q_ {i + 1}的正确候选者。

我们如何选择合适的候选人?选择输入中位置i之后的下一个。这就是loc(s)进来的地方。状态s的loc值正是我刚才所说的:从i开始的输入中的位置,其中接下来是与该状态相关的可行前缀。

重要的一点是,我们不想只分配新找到的(通过从H中拉出最小值)&#34;下一步&#34;可行前缀为Q_ {i + 1}(dp列i + 1的可用前缀)并继续下一个字符(i + 2)。这是我们必须设置阶段尽可能向前跳到 last 字符k(使用dp列k),例如Q_k = Q_ {i + 1}。我们按照算法A中的后缀边缘跳过。只有这次我们通过改变H:删除元素来记录我们将来使用的步骤,这与从S_i中删除元素和修改loc值相同。

函数loc的定义是裸的,他从未说过如何计算它。还没有提到的是loc(s)也是i的函数,正在处理的当前输入位置(他在纸张的早期部分从j跳到我这里的当前输入位置是无益的。)影响是loc随着输入处理的进行,更改

事实证明,定义的一部分适用于消除状态&#34;恰好发生了#34;因为状态在删除表格H时被标记为已删除。因此,对于这种情况,我们只需要检查标记。

另一种情况 - 未消除状态 - 要求我们在输入中向前搜索,寻找文本中未被其他字符串覆盖的下一个出现。这种覆盖的概念是为了确保我们始终只处理最长的&#34;可行的前缀。必须忽略较短的值以避免输出除最大匹配之外的输出。现在,搜索前进的声音很昂贵,但很高兴我们已经构建了一个后缀trie,这允许我们在O(| Key(s)|)时间内完成。必须仔细注释trie以返回相关的输入位置并避免出现Key(s),但它不会太难。当搜索失败时,他从未提到要做什么。这里loc(s)= \ infty,即它已被删除,应该从H中删除。

算法中最毛茸茸的部分可能是更新H来处理我们为可能的前缀添加新状态的情况,该前缀覆盖已经在H中的某些w的Key(w)。这意味着我们必须通过手术更新H中的(loc(w)=&gt; w)元素。结果后缀trie再次以其后缀边缘有效地支持它。

有了这一切,我们试试伪代码。

H = { (0 => root) }  // we use (loc => state) for min heap elements
until H is empty
  (j => s_j) = H.delete_min  // remove the min loc mapping from 
  (d, l) = dp(d', l', j) where (d',l') are cached at paraent(s_j)
  Compute |Q_j| = l[h], h = argmax(i).d[i]<=k as in the paper
  r = s_j
  while depth(r) > |Q_j|
    mark r eliminated
    H.delete (_ => r)  // loc value doesn't matter
  end while
  set cached d,l on node r

  // Add all the "next states" reachable from r by go-tos
  for all s = g(r, a) for some character a
    unless s.eliminated?
      H.insert (loc(s) => s)  // here is where we use the trie to find loc

      // Update H elements that might be newly covered
      w = f(s) // suffix transition
      while w != null
        unless w.eliminated?
          H.increase_key(loc(w) => w) // using explanation in Lemma 9.
          w = f(w) // suffix transition
        end unless
      end while
    end unless
  end for
end until

为了简单起见,我再次省略了输出。我不会说这是正确的,但它在球场上。有一件事是他提到我们应该只为节点处理Q_j而不是在#34;访问之前,&#34;但是我不明白&#34;访问了什么?意味着在这种情况下。我认为算法A定义的访问状态不会发生,因为它们已经从H中移除了。这是一个难题...

引理9中的increase_key操作被匆匆描述,没有证明。他声称在O(log | alphabet |)时间内可以进行最小操作,这给想象力留下了很多。

怪癖的数量让我想知道这不是本文的最终草案。它也是一个Springer出版物,如果它完全相同,这个在线副本可能会违反版权限制。可能值得在图书馆查看或支付最终版本以查看在最终审核期间是否有一些粗糙的边缘被淘汰。

这是我能得到的。如果您有具体问题,我会尝试澄清。

答案 1 :(得分:2)

这是启动此主题的原始问题。

Esko Ukkonen教授发表了paper关于后缀树的近似字符串匹配。他讨论了3种具有不同匹配时间的不同算法:

  1. 算法A:O(mq + n)
  2. 算法B:O(mq log(q) + size of the output)
  3. 算法C:O(m^2q + size of the output)
  4. 其中m是子字符串的长度,n是搜索字符串的长度,q是编辑距离。

    我一直在努力理解算法B,但是我在执行这些步骤时遇到了麻烦。有没有人有这种算法的经验?非常感谢示例或伪算法。

    特别是:

    1. size of the output在后​​缀树或输入字符串方面的含义是什么? 最终输出阶段列出Key(r)T的所有出现次数,适用于标记为输出的所有州r
    2. 查看算法C ,定义函数 dp (第4页);我不明白 i 代表什么索引。它没有初始化,似乎没有增加。
    3. 这就是我所相信的(我有待纠正):

      1. 在第7页,我们介绍了后缀树概念;状态实际上是后缀树中的一个节点: let root表示初始状态。
      2. g(a, c) = b 其中 a b 是树中的节点, c 是树中的字符或子字符串。所以这代表了一种转变;从 a 开始,按照 c 代表的边缘,我们转移到节点 b 。这称为转换。因此,对于下面的后缀树, g(root, 'ccb') = red node suffix tree for abccb
      3. Key(a) = edge sequence 其中 a 表示树中的节点。例如, Key(red node) = 'ccb' 。所以 g(root, Key(red node)) = red node
      4. Keys(Subset of node S) = { Key(node) | node ∈ S}
      5. 节点 a b f(a) = b 有一个后缀函数:for所有(或者可能存在) a root ,存在一个字符 c ,子字符串 x 和节点 b ,以便 g(root, cx) = a { {1}} 的。我认为,对于上面的后缀树示例,这意味着 g(root, x) = b 其中 f(pink node) = green node c = 'a'
      6. 有一个映射 x = 'bccb' ,其中包含后缀树中的节点和值对。该值由 H ;我还不确定如何评估这个功能。该词典包含尚未被删除的节点。
      7. loc(w) 指的是从 extract-min(H)获得 (node, loc(w)) 对中值最小的条目
      8. 算法的关键似乎与 H 的评估方式有关。我使用组合答案here构建了后缀树;但是,算法适用于后缀trie(未压缩后缀树)。因此,需要以不同方式维护和处理深度等概念。在后缀trie中,深度代表后缀长度;在后缀树中,深度将简单地表示树中的节点深度。