【问题标题】:Implementation of string pattern matching using Suffix Array and LCP(-LR)使用 Suffix Array 和 LCP(-LR) 实现字符串模式匹配
【发布时间】:2015-01-04 17:43:46
【问题描述】:

在过去的几周里,我试图弄清楚如何在另一个字符串中有效地找到一个字符串模式。

我发现很长一段时间以来,最有效的方法是使用后缀树。但是,由于这种数据结构在空间上非常昂贵,因此我进一步研究了后缀数组的使用(使用的空间要少得多)。不同的论文,例如“后缀数组:在线字符串搜索的新方法”(Manber & Myers,1993)状态,搜索子字符串可以在 O(P+log(N)) 中实现(其中 P 是模式的长度,N 是字符串的长度)通过使用二进制搜索和后缀数组以及 LCP 数组。

我特别研究了后一篇论文来理解搜索算法。 This answer 在帮助我理解算法方面做得很好(顺便把它变成了LCP Wikipedia Page)。

但我仍在寻找实现此算法的方法。尤其是上面提到的 LCP-LR 阵列的构造似乎非常复杂。

参考文献:

Manber & Myers,1993 年:Manber,乌迪; Myers, Gene, SIAM Journal on Computing, 1993, Vol.22(5), pp.935-948, http://epubs.siam.org/doi/pdf/10.1137/0222058

更新 1

只是强调一下我感兴趣的内容:我了解 LCP 阵列并找到了实现它们的方法。但是,“普通” LCP 阵列不适用于有效的模式匹配(如参考中所述)。因此,我对实现 LCP-LR 数组很感兴趣,这似乎比实现 LCP 数组要复杂得多

更新 2

添加了参考论文的链接

【问题讨论】:

  • 你需要用什么语言实现?
  • C/C++ 就可以了。或者任何可移植到 C/C++ 的语言实现,比我自己实现它更省力
  • 我稍微修改了这个问题,因为对实现和库的请求在这里通常是题外话(尽管显然你已经做了一些很好的研究,读者可能会更宽容一些)。跨度>
  • @Paddre c 实现与 c++ 实现有很大不同,因此请选择一种语言以获得更好的帮助。
  • 由于我的程序的其余部分将使用它是用 C++ 编写的,因此实现它的 C++ 方式就可以了。但是,由于我对高效率感兴趣,所以 C 实现也可以(即使我避免混淆 C 和 C++ 代码,为了效率,我会这样做;-))

标签: c++ c string pattern-matching


【解决方案1】:

可以帮助你的术语:enchanced suffix array,用于描述后缀数组与其他各种数组,以替换后缀树(lcp,child)。

这些可以是一些例子:

https://code.google.com/p/esaxx/ ESAXX

http://bibiserv.techfak.uni-bielefeld.de/mkesa/ MKESA

esaxx 似乎在做你想做的事,另外,它有示例 enumSubstring.cpp 如何使用它。


如果您查看引用的paper,它提到了一个有用的属性(4.2)。由于 SO 不支持数学,这里没有复制的意义。

我已经完成了快速实现,它使用了段树:

// note that arrSize is O(n)
// int arrSize = 2 * 2 ^ (log(N) + 1) + 1; // start from 1

// LCP = new int[N];
// fill the LCP...
// LCP_LR = new int[arrSize];
// memset(LCP_LR, maxValueOfInteger, arrSize);
// 

// init: buildLCP_LR(1, 1, N);
// LCP_LR[1] == [1..N]
// LCP_LR[2] == [1..N/2]
// LCP_LR[3] == [N/2+1 .. N]

// rangeI = LCP_LR[i]
//   rangeILeft  = LCP_LR[2 * i]
//   rangeIRight = LCP_LR[2 * i + 1]
// ..etc
void buildLCP_LR(int index, int low, int high)
{
    if(low == high)
    {
        LCP_LR[index] = LCP[low];
        return;
    }

    int mid = (low + high) / 2;

    buildLCP_LR(2*index, low, mid);
    buildLCP_LR(2*index+1, mid + 1, high);

    LCP_LR[index] = min(LCP_LR[2*index], LCP_LR[2*index + 1]);
}

【讨论】:

  • 它没有。 “增强后缀数组”是一个通用术语。 OP(以及我发布的赏金)正在寻找增强后缀数组的一个非常具体的方面,这就是 OP 中引用的论文中描述的 LCP-LR 数组的构造。具体来说,这可以在后缀数组上启用O(M + logN) 模式匹配。
  • @BurntSushi5:我的意思不是终端引导你更接近找到某种解决方案(它引导我到 ESAXX,它展示了如何有效地从字符串中查找子字符串)。
  • @BurntSushi5:如果您对这篇具体的论文及其具体实现感兴趣,我建议您在此处添加论文。另外,我确实看了一下:如果您已经设法通过使用该论文(通过区间树)创建 LCP,那么您已经 计算 LCP-LR 的值(您必须复制它们从区间树排列)。你到底卡在哪里了?
  • 我觉得 OP 已经非常具体了。我们想做有效的模式匹配(在O(m + logn) 时间)。引用的论文提供了执行此操作的算法,但它需要 LCP-LR 阵列。所以我们想要的是:给定一个后缀数组(没有增强,只是普通的 ol' 后缀数组)和 LCP 数组,如何在O(n) 时间计算 LCP-LR 数组?
  • 非常好!赏金会在几个小时后结束,我没有时间实际验证它,但对我来说这看起来很合理,所以我给你的答案是赏金。 :-) 它当然给了我一些线索。当我实际实施它时,如有必要,我会发布任何更新。谢谢! :-)
【解决方案2】:

这是一个相当简单的 C++ 实现,尽管 build() 过程在 O(N lg^2 N) 时间构建后缀数组。 lcp_compute() 过程具有线性复杂性。我在很多编程比赛中都使用过这段代码,它从来没有让我失望过 :)

#include <stdio.h>
#include <string.h>
#include <algorithm>

using namespace std;

const int MAX = 200005;
char str[MAX];
int N, h, sa[MAX], pos[MAX], tmp[MAX], lcp[MAX];

bool compare(int i, int j) {
  if(pos[i] != pos[j]) return pos[i] < pos[j]; // compare by the first h chars
  i += h, j += h; // if prefvious comparing failed, use 2*h chars
  return (i < N && j < N) ? pos[i] < pos[j] : i > j; // return results
}

void build() {
  N = strlen(str);
  for(int i=0; i<N; ++i) sa[i] = i, pos[i] = str[i]; // initialize variables
  for(h=1;;h<<=1) {
    sort(sa, sa+N, compare); // sort suffixes
    for(int i=0; i<N-1; ++i) tmp[i+1] = tmp[i] + compare(sa[i], sa[i+1]); // bucket suffixes
    for(int i=0; i<N; ++i) pos[sa[i]] = tmp[i]; // update pos (reverse mapping of suffix array)
    if(tmp[N-1] == N-1) break; // check if done
  }
}

void lcp_compute() {
  for(int i=0, k=0; i<N; ++i)
    if(pos[i] != N-1) {
      for(int j=sa[pos[i]+1]; str[i+k] == str[j+k];) k++;
      lcp[pos[i]] = k;
      if(k) k--;
    }
}

int main() {
  scanf("%s", str);
  build();
  for(int i=0; i<N; ++i) printf("%d\n", sa[i]);
  return 0;
}

注意:如果你想让build()过程的复杂度变成O(N lg N),你可以用基数排序代替STL排序,但这会使代码复杂化。 p>

编辑:对不起,我误解了你的问题。虽然我没有用后缀数组实现字符串匹配,但我想我可以为你描述一个简单的非标准但相当有效的字符串匹配算法。您将获得两个字符串,textpattern。给定这些字符串,您将创建一个新字符串,我们称之为concat,它是两个给定字符串的连接(首先是text,然后是pattern)。您在concat 上运行后缀数组构造算法,然后生成普通的 lcp 数组。然后,在刚刚构建的后缀数组中搜索长度为pattern.size() 的后缀。让我们称它在后缀数组pos 中的位置。然后你需要两个指针lohi。在开始lo = hi = pos。你减少lolcp(lo, pos) = pattern.size() 并且你增加hilcp(hi, pos) = pattern.size()。然后在[lo, hi] 范围内搜索长度至少为2*pattern.size() 的后缀。如果你找到它,你就找到了匹配。否则,不存在匹配项。

编辑[2]:我一有实现就会回来...

编辑[3]:

这里是:

// It works assuming you have builded the concatenated string and
// computed the suffix and the lcp arrays
// text.length() ---> tlen
// pattern.length() ---> plen
// concatenated string: str

bool match(int tlen, int plen) {
  int total = tlen + plen;
  int pos = -1;
  for(int i=0; i<total; ++i)
    if(total-sa[i] == plen)
      { pos = i; break; }
  if(pos == -1) return false; 
  int lo, hi;
  lo = hi = pos;
  while(lo-1 >= 0 && lcp[lo-1] >= plen) lo--;
  while(hi+1 <  N && lcp[hi] >= plen) hi++;
  for(int i=lo; i<=hi; ++i)
    if(total-sa[i] >= 2*plen)
      return true;
  return false;
}

【讨论】:

  • 我已经有了一个高效的构造实现。但是,正如问题中所述,我对可用于有效模式匹配的实现特别感兴趣。据我所知,仅使用“普通” LCP 和阵列是不可能的,因为无法检查每个可能的匹配项。如果我在这里错了,请纠正我;-)
  • @Paddre 我将match 过程的代码添加到我的答案中
  • 我可以设法测试它。首先:您应该提到,终止符号(通常称为#$)必须由用户添加,而不是SA 构造机制的一部分。一旦我发现你的代码很清楚,但不是我之前与许多其他 SA/LCP 实现合作过的预期。第二:匹配似乎无法正常工作。我尝试将"vwvwxy# 作为text"vwx" 作为pattern(即concat = "vwvwxy#vwx") and match()`返回false
  • @Paddre 您不应该添加终止符号。在您提到的情况下,concat 应该是"vwvwxyvwx"。试一试,你会看到match() 返回true
  • 不。与str="vwvwxyvwx"match(6,1) 一起返回false。 (SA:{0,6,2,1,7,3,8,4,5}
【解决方案3】:

Here 是一篇不错的帖子,其中包含一些代码,可帮助您更好地理解 LCP 数组和比较实现。

我了解您的愿望是代码,而不是实现您自己的。 虽然由 Sedgewick 和 Wayne 从他们的 Algorithms booksite 用 Ja​​va this is an implementation of Suffix Array with LCP 编写。它应该可以为您节省一些时间,并且移植到 C/C++ 应该不会非常困难。

LCP array construction 为那些可能想要更多关于算法的信息的人使用。

【讨论】:

  • 不幸的是,它们都没有提供有关构建适合高效模式匹配的 LCP 数组的信息
【解决方案4】:

我认为@Erti-Chris Eelmaa 的算法是错误的。

L ... 'M ... M ... M' ... R
       |-----|-----|

左子范围和右子范围应该全部包含M。因此我们不能对LCP-LR数组进行正常的段树划分。 代码应该是这样的

def lcp_from_i_j(i, j): # means [i, j] not [i, j)
    if (j-i<1) return lcp_2_elem(i, j)
    return lcp_merge(lcp_from_i_j(i, (i+j)/2), lcp_from_i_j((i+j)/2, j)

左右子范围重叠。段树支持范围最小查询。但是,[a,b] 之间的范围 min 不等于 [a,b] 之间的 lcp。 LCP 数组是连续的,简单的 range-min 是行不通的!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-23
    • 1970-01-01
    • 2018-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-11
    相关资源
    最近更新 更多