使用后缀数组和LCP(-LR)实现字符串模式匹配

时间:2015-01-04 17:43:47

标签: c++ c string pattern-matching

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

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

我特别研究了后一篇论文来理解搜索算法。 This answer在帮助我理解算法方面做得非常出色(并顺便将其纳入LCP Wikipedia Page)。

但我仍在寻找实现此算法的方法。特别是上述LCP-LR阵列的构造似乎非常复杂。

参考文献:

Manber&迈尔斯,1993年:曼伯,乌迪; 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

添加了参考文件的链接

4 个答案:

答案 0 :(得分:6)

可以帮助您的终端: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]);
}

答案 1 :(得分:1)

这是一个在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排序,但这会使代码。

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

编辑[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;
}

答案 2 :(得分:0)

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

我理解你的愿望是代码,而不是实现自己的代码。 尽管Sedgewick和Wayne用他们的this is an implementation of Suffix Array with LCP用Java Algorithms booksite编写。它应该节省您一些时间,并且不应该非常难以移植到C / C ++。

对于那些可能需要有关该算法的更多信息的人来说,

LCP array construction为伪。

答案 3 :(得分:0)

我认为@ 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阵列是连续的,简单的范围min不起作用!