后缀数组学习小结(已经死了)

一直想学习后缀数组,但是lrj的算法经典上只给出了原理和代码,代码上面没有注释,让人很难读懂。所以我对后缀数组的了解一直停留在知道这个东西和倍增法的原理,至于板子和套路完全不知。
最近还是死啃了板子,把我现在掌握的东西记成博文,也方便自己和大家。
本人也不是很会这个,如果dalao们发现了问题,请指正!

首先,倍增法在算法竞赛上已经讲得非常详细了,在此我也不再赘述,大家自己阅读书吧。我在这就把lrj给出的代码注释一下,顺便改正一下他在写的里面的错误。

这是刘汝佳给出的代码(注意,在注释!!!!!的地方,书上的 i i 的起始标记写错了,应该是1而不是0)

void build_sa(int m) {
    int *x = t, *y = t2;
    for (int i = 0; i < m; i ++) c[i] = 0;
    for (int i = 0; i < n; i ++) c[x[i] = s[i]] ++;
    for (int i = 1; i < m; i ++) c[i] += c[i - 1];
    for (int i = n - 1; i >= 0; i --) sa[-- c[x[i]]] = i;
    for (int k = 1; k < n; k <<= 1) {
        int p = 0;
        for (int i = n - k; i < n; i ++) y[p ++] = i;
        for (int i = 0; i < n; i ++) if (sa[i] >= k) y[p ++] = sa[i] - k;
        for (int i = 0; i < m; i ++) c[i] = 0;
        for (int i = 0; i < n; i ++) c[x[y[i]]] ++;
        for (int i = 1; i < m; i ++) c[i] += c[i - 1];  //!!!!!
        for (int i = n - 1; i >= 0; i --) sa[-- c[x[y[i]]]] = y[i];
        swap(x, y);
        p = 1; x[sa[0]] = 0;
        for (int 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;
    }
}

如果是第一次看这个代码,相信你也我一开始一样一脸懵逼,下来我细细讲解一下各个数组的含义。

首先,倍增法需要开4倍的辅助空间,其中有结果数组sa,临时数组t,t2,计数数组c。
在具体的函数中,刘汝佳用x和y分别指代了t和t2。

下面是讲解每个数组的具体含义:
假设原串s,s的长度为n。
sa[i],指的是在s中的所有后缀中,字典序第i小的后缀在原串中的起始位置下标。 0<=i<n,0<=sa[i]<n 0 <= i < n , 0 <= s a [ i ] < n
t和t2只是临时的数组,没有十分大的意义;
但是在函数中,x和y数组有具体的意义
x[i],指在倍增的过程中,当前计算出来的第i位的第一关键字的字符值。注意,随着算法的进展,x[i]一直会发生变化。第一关键字允许出现重复。
y[i],指在倍增的过程中,当前计算出来的第i小的第二关键字在s中的所在位置。注意,随着算法的进展,y[i]一直会发生变化。第i小和第i + 1小的第二关键字的大小允许相同,但是仍需要给定不同的位次。
c[i],指倍增过程中,第一关键字的字符值小于等于i的个数。

解释了一下各个数组的含义,我们开始讲解函数每一步的实际含义。

char tmp[maxn];
int s[maxn];
int rank_sa[maxn], height[maxn];
int sa[maxn], t[maxn], t2[maxn], c[maxn], n;


// 以字符值数组s构造sa,字符值从0-m-1
void build_sa(int m) {                                             //字符值最大值
    int *x = t, *y = t2;
    for (int i = 0; i < m; i ++) c[i] = 0;                         //清空计数数组
    for (int i = 0; i < n; i ++) c[x[i] = s[i]] ++;                //统计一下每个第一关键字的个数
    for (int i = 1; i < m; i ++) c[i] += c[i - 1];                 //计算第一关键字个数的前缀和
    for (int i = n - 1; i >= 0; i --) sa[-- c[x[i]]] = i;          //粗配后缀数组  因为对于一个第一关键字x的后缀,最多有c[x] - 1个后缀在前,那就把当前的后缀认为是第c[x] - 1(从0开始)个好了;同时,i递减代表了第一字符值大小递减,而c[x]是根据x递增的
    for (int k = 1; k < n; k <<= 1) {                              //倍增长度
        int p = 0;                                                 //p作为排序顺序,也作为数组下标
        for (int i = n - k; i < n; i ++) y[p ++] = i;              //因为对于一个固定的k,后面的k个的第二关键字是0(看书),所以第二关键字一定排在前k个
        for (int i = 0; i < n; i ++) if (sa[i] >= k) y[p ++] = sa[i] - k; //对于剩下的,第sa[i] - k个的第二关键字就是第sa[i]个的第一关键字,所以按照sa的大小(记住sa是从小到大的)顺序给剩下的n-k个付上第二关键字,赋值的先后决定了y的大小排列
        for (int i = 0; i < m; i ++) c[i] = 0;                     //下四行同上。只是i变成了y[i],因为不仅需要第一关键字排序,也需要按照第二关键字排列。
        for (int i = 0; i < n; i ++) c[x[y[i]]] ++;                
        for (int i = 1; i < m; i ++) c[i] += c[i - 1];
        for (int i = n - 1; i >= 0; i --) sa[-- c[x[y[i]]]] = y[i];
        swap(x, y);                                                //交换x,y;  现在的y变成了先前的x数组,现在的x数组暂时失去了意义。
        p = 1; x[sa[0]] = 0;
        for (int i = 1; i < n; i ++)                               //p也被重新附上了新的意义,重新标第一关键字的大小。现在的第一关键字需要根据之前的第一与第二关键字字典序排序标号。
            x[sa[i]] = (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k]) ? p - 1 : p ++; //y[sa[i - 1]] == y[sa[i]],第一关键字相同,y[sa[i - 1] + k] == y[sa[i] + k]第二关键字相同,同时相同标相同的号,否则号递增。因为sa数组是从小到大的,所以顺序根据sa确定了
        if (p >= n) break;
        m = p;                                                     //现在的字符值变成了p个
    }
}

这样我感觉应该讲清楚了。。
还是需要自己花一定的时间去理解,有一个大致的概念,应该能够方便理解代码的含义吧。
总结一下后缀数组的倍增法,这里面的关系挺绕的,也挺复杂的。有很多变量有双重的身份,我觉得这样十分巧妙,非常佩服。

只求出了一个字符串的后缀数组没有任何意义,下来就是体现后缀数组意义的地方了。

A、多模式串匹配
处理了原串的后缀数组,然后就能二分查询了

int m;
int cmp_suffix(char *pat, int p) {
    return strncmp(pat, s + sa[p], m);
}

int find_st(char *P) {
    m = strlen(P);
    if (cmp_suffix(P, 0) < 0) return -1;            //比最小的小或比最大的大都不行
    if (cmp_suffix(P, n - 1) > 0) return -1;
    int lb = 0, ub = n - 1;
    while (lb <= ub) {                              //后缀随sa递增,可以二分
        int mid = lb + (lb + ub) >> 1;
        int res = cmp_suffix(P, mid);
        if (!res) return mid;
        if (res < 0) ub = mid - 1;
        else lb = mid + 1;
    }
    return -1;
}

B、求公共子串
需要用到最长公共前缀。我觉得白书上解释的挺详细的,应该挺好懂,就不赘述了。
白书上的会数组越界,现在已经改正

void getHeight(int n) {
    int k = 0;
    height[0] =  0;  
    for (int i = 0; i < n; i ++) rank_sa[sa[i]] = i;
    for(int i = 0; i < n - 1; i ++) {  
        int j = sa[rank_sa[i] - 1];  
        while(i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
        height[rank_sa[i]] = k;
        k = max(0, k - 1);  
    }  
}

求公共子串的套路就是把所有的给的串合成一个大字符串,每个小串间用一个不存在的字符隔开,然后求sa,求height,按要求找需要的值就行了。

你可能感兴趣的:(后缀数组)