后缀数组

我怕爱的太早我们不能终老 提交于 2020-11-22 17:30:34

后缀数组

先下几个常见的定义

$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)$的复杂度

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!