后缀数组之倍增算法

来源:互联网 发布:mysql 自带记录行号 编辑:程序博客网 时间:2024/05/01 19:39

后缀数组是后缀树的替代品。它不仅时间效率高,而且代码比较简单,不易写错。
后缀数组可以查询模板串P[0...m1],是否为文本串s[0...n1]的子串,并在n很大,查询次数多时效率很高。
我们定义:

  • 后缀k:后缀s[k…n-1]
  • k前缀:对于字符串u,u的前k个字符组成的字符串(k过大即为整个u)
  • 后缀数组(sa[i]):对所有后缀排序之后排名第i(0i<n)的后缀的编号
  • 名词数组(rank[i]):后缀i的排名。

我们主要研究的是后缀数组的构造,因为文本串长度n比较大,所以我们要想办法设计出快速的算法,构造出后缀数组以后,我们只要在后缀数组中二分就可以找到是否存在一个后缀的前缀(即子串)与模板串匹配就行了(因为后缀数组是有序的),查询的时间复杂度为O(mlogn)
倍增算法是构造后缀数组代码比较简单且容易理解一种算法。

倍增算法的主要思路是:
利用如下性质:

  • 后缀i和后缀j的前2k个字符相等<=>后缀i和后缀j的前k个字符相等并且后缀i+k和后缀j+k的前k个字符相等
  • 后缀i的前2k个字符小于后缀j的前2k个字符<=>(后缀i的前k个字符小于后缀j的前k个字符)或(后缀i的前k个字符等于后缀j的前k个字符且后缀i+k的前k个字符小于后缀j+k的前k个字符)

虽然描述有些冗杂,但还是很好理解的。
我们发现已知所有后缀前k个字符的大小关系就可以推导出所有后缀的前2k个字符的大小关系。
我们把后缀i的前k个字符的排名做为第一关键字,把后缀i+k的前k个字符的排名作为第二关键字,然后排序就可以求出后缀i的前2k个字符的排名。
所以用倍增的方法对每个字符开始的长度为2k的子字符串进行排序,求出排名,即rank值。k从0开始,每次加1,当2k大于n以后,每个字符开始的长度为2k的子字符串便相当于所有的后缀。并且这些子字符串都一定已经比较出大小,即rank值中没有相同的值,那么此时的rank值就是最后的结果。每一次排序都利用上次长度为2k-1的字符串的rank值,那么长度为2k的字符串就可以用两个长度为2k-1的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为2k的字符串的rank值。

对于排序,最好的选择是使用基数排序(如果不知道基数排序,最好去看看,如果不懂的话很难看懂程序),然后我们就求出了后缀数组了。。。

代码如下:

#include<cstdio>#include<algorithm>#include<cstring>using namespace std;const int MAXN = 10000010;char s[MAXN], P[MAXN];int sa[MAXN], t[MAXN], t2[MAXN], c[MAXN], n, m;void build_sa(int m) {    int k, i, *x = t, *y = t2;    for(i = 0; i < m; i++) c[i] = 0;    for(i = 0; i < n; i++) c[x[i] = s[i]]++;    for(i = 1; i < m; i++) c[i] += c[i-1];    for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;    for(k = 1; k <= n; k <<= 1) {    int p = 0;    for(i = n-k; i < n; i++) y[p++] = i;    for(i = 0; i < n; i++) if(sa[i] >= k) y[p++] = sa[i]-k;    for(i = 0; i < m; i++) c[i] = 0;    for(i = 0; i < n; i++) c[x[y[i]]]++;    for(i = 1; i < m; i++) c[i] += c[i-1];    for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];    swap(x, y);    p = 1;    x[sa[0]] = 0;    for(i = 1; i < n; i++)         x[sa[i]] = y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k] ? p-1 : p++;    if(p >= n) break;    m = p;    }}int cmp(char* P, int i) {    return strncmp(P, s+sa[i], m);}int find(char* P) {    m = strlen(P);    if(cmp(P, 0) < 0) return -1;    if(cmp(P, n-1) > 0) return -1;    int L = 0, R = n-1;    while(L <= R) {        int mid = (L+R)>>1, res = cmp(P, mid);        if(res == 0) return sa[mid];        if(res < 0) R = mid-1;        else L = mid+1;    }    return -1;}int main() {    int i, t;    scanf("%s", s);    n = strlen(s);    build_sa(200);    scanf("%d", &t);    for(i = 1; i <= t; i++) {        scanf("%s", P);        printf("%d\n", find(P));    }    return 0;}

其实后缀数组的代码可读性普遍不强。。。
知道原理也很难看懂。。。
请大家自己慢慢研究。。。

1 0
原创粉丝点击