后缀数组被称为字符串处理神器,要解决字符串问题,一定要掌握它。(我这里的下标全部都是从1开始)

首先后缀数组要处理出两个数组,一个是sa[],sa[i]表示排名第i为的后缀的起始位置是什么,rank[i]表示第i个字符为起始点的后缀,它的排名是什么。可以知道sa[rank[i]] = i; rank[sa[i]] = i;

后缀数组 & 题目

由于每个后缀各不相同,至起码长度不同,所以每个后缀是不可能相等的。

 

解除一个值,就能在O(n)时间内得到另外一个。

定义:suffix(i)表示从[i, lenstr]这个后缀。

普通排序复杂度显然是O(n^2),因为快排最坏情况也是O(n^2)。考虑运用字符串的特点?。这里考虑倍增法。为什么能用倍增呢?因为它充分利用了前面已经得到的信息。因为字符串的比较,都是从头到尾比较的,那么如果已经知道一个字符串前len / 2部分的比较结果,对于后加进来的后len / 2部分,还需要继续比较前len / 2部分吗?答案是不用的。所以,考虑倍增,先得到1个字符自己的rank,然后考虑2个字符的时候,用后一个字符的rank来作为第二关键字,进行排序即可。

后缀数组 & 题目

对关键字排序选用的是基数排序,因为它可以在O(n + maxnum)的时间内排好序。然后为了预防越界,就是aaaa这样,suffix(1)和suffix(2)比较的话,前3个都是匹配的,然后明显是"aaa",suffix(2)比较小,所以就在末尾加上一个0,来预防越界,也能解决排名问题。

关于后缀数组的字符数组为什么用int str[]这样:


ans:因为有时候解题的时候,需要把n个串连接起来,那么你每两个串之间就要加上一些不会出现的字符,来防止越界。

就是aaa%aaa%aaa这样是没用的,因为两个%会相等,使得LCP变大。aaa$aaa%aaa#才是正确的打开方式。

那么问题来了,n很大,1000个左右,你用char字符无能为力了,所以这个时候只能用int str[]了。

但是一般的题,都是1个或者两个字符串而已,用char str[]是足够的。

题目就是那个POJ 3294了。

 

book[]大小,用于基数排序,起码要大于lenstr,因为要记录rank[],而rank[]会有lenstr那么大

const int maxn = 20000 + 20;
int a[maxn];
int sa[maxn];
int x[maxn];
int y[maxn];
int book[10000000 + 20];
bool cmp(int r[], int a, int b, int len) {
    return r[a] == r[b] && r[a + len] == r[b + len];
}
void da(int str[], int sa[], int lenstr, int mx) {
    int *fir = x, *sec = y, *ToChange;
    for (int i = 0; i <= mx; ++i) book[i] = 0; //清0
    for (int i = 1; i <= lenstr; ++i) {
        fir[i] = str[i]; // 开始的rank数组,只保留相对大小即可,开始就是str[]
        book[str[i]]++; //统计不同字母的个数
    }
    for (int i = 1; i <= mx; ++i) book[i] += book[i - 1]; //统计 <= 这个字母的有多少个元素
    for (int i = lenstr; i >= 1; --i) sa[book[fir[i]]--] = i; // <=str[i]这个字母的有x个,那么,排第x的就应该是这个i的位置了。
    //倒过来排序,是为了确保相同字符的时候,前面的就先在前面出现。
    //倍增法求sa[],复杂度O(nlogn),p是第二个关键字0的个数
    for (int j = 1, p = 1; p <= lenstr; j <<= 1, mx = p) { //字符串长度为j的比较
        //上面已经求出了第一个关键字了,现在求第二个关键字,然后合并(合并的时候按第一关键字优先合并)
        p = 0;
        for (int i = lenstr - j + 1; i <= lenstr; ++i) sec[++p] = i; //这些位置,再跳j格就是越界了的,所以第二关键字是0,排在前面
        for (int i = 1; i <= lenstr; ++i)
            if (sa[i] > j) //如果排名第i的起始位置在长度j之后
                sec[++p] = sa[i] - j; //减去这个长度j,表明第sa[i] - j这个位置的第二个关键字是从sa[i]处拿的,排名靠前也正常,因为sa[i]排名是递增的
        //sec[]保存的是下标,现在对第一个关键字排序
        for (int i = 0; i <= mx; ++i) book[i] = 0; //清0
        for (int i = 1; i <= lenstr; ++i) book[fir[sec[i]]]++;
        for (int i = 1; i <= mx; ++i) book[i] += book[i - 1];
        for (int i = lenstr; i >= 1; --i) sa[book[fir[sec[i]]]--] = sec[i]; //因为sec[i]才是对应str[]的下标
        //现在要把第二关键字的结果,合并到第一关键字那里。同时我需要用到第一关键字保存的记录
        //所以用指针交换的方式达到快速交换数组中的值
        ToChange = fir; fir = sec; sec = ToChange;
        fir[sa[1]] = 0; //固定的是0 因为sa[1]固定是lenstr那个0
        p = 2;
        for (int i = 2; i <= lenstr; ++i) //fir是当前的rank值,sec是前一次的rank值
            fir[sa[i]] = cmp(sec, sa[i - 1], sa[i], j) ? p - 1 : p++;
    }
    return ;
}
int rank[maxn];
int height[maxn];
//height[i]:表示suffix(sa[i - 1]) 和 suffix(sa[i]) 的LCP
//就是两个排名紧挨着的后缀的LCP
//sa[rank[i]] = i;  rank[sa[i]] = i;
void CalcHight(int str[], int sa[], int lenstr) {
    for (int i = 1; i <= lenstr; ++i) rank[sa[i]] = i;
    int k = 0;
    for (int i = 1; i <= lenstr - 1; ++i) { //最后一位不用算,最后一位排名一定是1,然后sa[0]就尴尬了
        k -= k > 0;
        int j = sa[rank[i] - 1]; //排名在i前一位的那个串,相似度最高
        while (str[j + k] == str[i + k]) ++k;
        height[rank[i]] = k;
    }
    return ;
}
View Code

相关文章: