后缀数组之倍增算法

后缀数组是后缀树的替代品。它不仅时间效率高,而且代码比较简单,不易写错。
后缀数组可以查询模板串 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;
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值