后缀数组

后缀数组

先下几个常见的定义

\(s(i, j)\)表示\([i, j]\)形成的连续子串

\(suf[i]\)表示以\(i\)为开头的后缀

\(rank\)数组:\(rank[i]\)表示将\(1\sim n\)的后缀排序后,\(suf[i]\)的排名

\(sa\)数组:\(sa[i]\)表示将\(1 \sim n\)的后缀排序后,排第\(i\)的在哪里

举个例子:

sort(a + 1, a + n + 1, cmp);

当我们采用上述代码之后,\(a\)数组存下的实际就是排第\(i\)的在哪里


\(sa\)数组不会相同,因为后缀的长度互不相同

在假想状态下,我们考虑在字符串的某尾加上无限个\(0\)

这样子,我们可以使得两个后缀具有相同的长度,便于比较

比如字符串"\(abaa\)"

我们实际上是在比较这4个字符串的排名:

\(abaa,baa0,aa00,a000\)


先看一个有趣的事情:
对于字符串\(S = S_1 + S_2, T = T_1 + T_2\),且\(|S_1| = |S_2|, |T_1| = |T_2|\)
如果\(S_1 < T_1\),那么\(S < T\)
如果\(S_1 = T_1\;and\;S_2 < T_2\),那么\(S < T\)


这要怎么利用呢?

也就是说,我们现在把所有的后缀都看做是长度为\(n\)的字符串

我们先处理出所有的\(s(i, i)\)的字典序排名,如果不存在\(s(i, i) = s(j, j)\),那么我们的序排好了

否则,我们可以利用所有的\(s(i, i)\),按照上面的排序方式,得出所有的\(s(i, i + 2^1 - 1)\)的字典序排名

同样,如果不存在\(s(i, i + 2^1 - 1) = s(j, j + 2^1 - 1)\),那么我们的序就排好了

否则,合并出\(s(i, i + 2^2 - 1)\),然后再去判断

依次类推

当我们合并到\(s(i, i + 2^k - 1)\;(2^k \geq n)\)时,我们一定能判断出字典序排名

如果合并的时候,我们的复杂度可以做到\(O(n \log n)\),那么总体而言,就能做到\(O(n \log ^2 n)\)

如果合并的时候,我们能做到\(O(n)\),那么总体而言,就能做到\(O(n \log n)\)

1542304416806

可以对着上面这张经典的图理解一下


在描述\(O(n \log n)\)的鬼畜写法之前,我们先来看看\(O(n \log^2 n)\)的写法

inline bool cmp(int x, int y) { return P[x] < P[y]; }
inline void Suffix_sort() {
    for(int i = 1; i <= n; i ++) sa[i] = i;
    for(int i = 1; i <= n; i ++) rk[i] = s[i];
    //初始化sa和rank
    for(int k = 1; k <= n; k <<= 1) {
        //倍增
        for(int i = 1; i <= n; i ++) 
            P[i] = make_pair(rk[i], rk[i + k]);
        //通过stl的pair+sort来实现双关键字排序
        sort(sa + 1, sa + n + 1, cmp);
        int tmp = 1; rk[sa[1]] = 1;
        for(int i = 2; i <= n; i ++)
            rk[sa[i]] = (P[sa[i]] == P[sa[i - 1]]) ? tmp : ++ tmp;
        //排完序之后,按照字典序的顺序对每个点重新计算rank
        if(tmp >= n) break;
        //如果当前的排名已经 >= n,代表不存在两个相同的量,也就是排完了
    }
}

(它在luogu上跑过了1000000)

请确保你在明白了\(O(n \log^2 n)\)的写法后,再继续往下阅读

我们只需要把上面的\(sort\)换成基数排序即可

基数排序原理十分的好懂,请自行了解

void sort(int *a, int n, int m) {
    for(int i = 1; i <= n; i ++) cnt[p2[i]] ++;
    for(int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
    for(int i = 1; i <= n; i ++) b[cnt[p2[i]] --] = i;
    //b用来暂时存储对第二关键字排完序之后的结果
    for(int i = 0; i <= m; i ++) cnt[i] = 0;
    for(int i = 1; i <= n; i ++) cnt[p1[i]] ++;
    for(int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
    for(int i = n; i >= 1; i --) a[cnt[p1[b[i]]] --] = b[i];
    //这里一定要倒叙枚举
    for(int i = 0; i <= m; i ++) cnt[i] = 0;
}

inline void Suffix_sort() {
    for(int i = 1; i <= n; i ++) sa[i] = i;
    for(int i = 1; i <= n; i ++) rk[i] = s[i];
    int m = 128; //m初始化为字符集的大小
    for(int k = 1; k <= n; k <<= 1) {
        for(int i = 1; i <= n; i ++) 
            p1[i] = rk[i], p2[i] = rk[i + k];
        sort(sa, n, m);
        int tmp = 1; rk[sa[1]] = 1;
        for(int i = 2; i <= n; i ++)
            rk[sa[i]] = 
            	(p1[sa[i]] == p1[sa[i - 1]] && p2[sa[i]] == p2[sa[i - 1]]) ? 
            		tmp : ++ tmp;
        if(tmp >= n) break;
        m = tmp;
    }
}

那么我们能不能进一步优化呢?

当然是可以的,可以发现上面代码中的\(rk, p1, p2\)数组其实只需要保留两个即可

并且,对第二关键字实际上并不需要基排,只需要调用上一次的\(sa\)数组就可以得到结果

然而经过测试,发现在\(10^6\)的数据下只有\(20ms\)的常数差距

因此实际上没有必要学习网上流传的写法...


经过这么一段长长的文字,你终于懂了后缀数组,但是给后缀排序有啥用呢?

我们需要引入一个更强大的数组

\(lcp(i, j)\)表示后缀\(i\)和后缀\(j\)的最长公共前缀

\(height\)数组,表示\(lcp(suf[sa[i]], suf[sa[i - 1]])\),即字典序第\(i\)小和字典序第\(i - 1\)小的最长公共前缀


一个十分重要的性质
对于后缀\(suf[i]\)\(suf[j]\),满足\(lcp(i, j) = min(height[k])\;(k \in[rk[i] + 1, rk[j]])\)


考虑证明:

首先证明,\(lcp(i, j) \leq min(height[k])\;(k \in[rk[i] + 1, rk[j]])\)

我们记\(min(height[k]) = h\)

如果\(lcp(i, j) > h\),那么对于\(k \in [rk[i] + 1, rk[j]]\)而言

我们记\(lcp(i, j) = L\)

由于\(k\)的排名处于\(i\)\(j\)之间,并且\(i\)\(j\)的前\(L\)都相同,必然有\(k\)包含\(L\)这个前缀

否则,\(k\)的排名由于在\(L\)之前就出现了不同,因此要么在\(i\)之前,要么在\(j\)之后

因此,有\(height[k] = L > h = min(height[k])\)

这不可能,因此\(lcp(i, j) \leq h\)

然后证明,可以取到上界

这是因为\(height[i] \geq h\),可以看做\(suf[sa[i]]\)\(suf[sa[i - 1]]\)至少有长为\(h\)的公共前缀

公共前缀满足传递性,因此可以取到上界


那么怎么求\(height\)数组呢?

如果暴力的求解,复杂度显然是\(O(n^2)\)

我们可以很清楚的知道\(suf[sa[i]]\)\(suf[sa[i - 1]]\)在字符串构成的联系不如\(suf[i]\)\(suf[i - 1]\)的联系

毕竟,\(suf[i]\)\(suf[i - 1]\)只相差了一个字符

那么,我们直接按照下标顺序来计算的时候,可以发现


我们记\(h[i]\)表示\(suf[i]\)和字典序排在它前面的最长公共前缀

那么\(h[i] \geq h[i - 1] - 1\)

比如现在正在求\(h[i]\),排在\(i\)之前的后缀是\(suf[j]\)

后缀\(i\)\(abc...\),后缀\(j\)\(aba...\),那么\(h[i] = 2\)

\(h[i + 1]\)时,由于\(h[i] \geq 1\;(h[i] = 0可以无视)\)

因此,去掉首字母的后缀\(i + 1\)\(bc...\),后缀\(j + 1\)\(ba...\)

可以发现,\(j + 1\)还是保持排在\(i + 1\)的前面,不妨设排在\(i + 1\)前面的后缀为\(k\)

那么,一定有\(h[i + 1] = lcp(i + 1, k) \geq lcp(i + 1, j + 1) = h[i] - 1\)


和求后缀数组比起来,求\(height\)显得十分的简洁

void Solve() {
    for(int i = 1, k = 0; i <= n; i ++) {
        if(k) k --; //此时的k相当于h[i],得到的新k相当于h[i] - 1
        int j = sa[rk[i] - 1];
        while(s[j + k] == s[i + k]) k ++;
        height[rk[i]] = k; // = h[i]
    }
}

由于每次求\(h[i]\)时,\(k\)指针都只会\(- 1\),也就是增加\(1\)的势能

而每次\(k\)往前挪移的时候,都需要\(1\)的势能

那么,势能总量是\(O(n)\)的,也就是求\(height\)数组只需要\(O(n)\)的复杂度

posted @ 2018-11-16 13:22  remoon  阅读(280)  评论(0编辑  收藏  举报