后缀数组(SA)学习笔记
后缀排序
给定字符串 \(a\),我们要将它的 \(n\) 个后缀按字典序排序,形成一个 \(sa\) 数组,\(sa_i\) 表示排名第 \(i\) 的后缀左端点。顺便搞出一个 \(rk=sa^{-1}\)。
明确一点,由于各后缀长度不等,所以排序结果是唯一的。
基数排序
后缀排序需要用到。
考虑对一个整数序列 \(x\) 排序。我们考虑将它扔进一个桶里,\(cnt_i\) 表示值为 \(i\) 的个数。对 \(cnt\) 做一遍前缀和得到 \(Cnt_i\),那么此时 \(Cnt_i\) 表示 \(\leq i\) 的个数。
先令 \(x\) 为任意一个顺序。然后从后往前遍历,将答案序列 \(x'\) 的第 \(Cnt_{x_i}\) 位放上 \(x_i\) 并令 \(Cnt_{x_i}\gets Cnt_{x_i}-1\)。不难发现这个排序算法是稳定的,复杂度为 \(\mathrm O(n+v)\),其中 \(v\) 是值域大小。
但是有的时候值域过大怎么办呢。我们考虑设一个 \(base\),将每个数分成 \(\log_{base}v\) 份,然后从高往低设为关键字排序。那么就归约成了更强的问题:给定若干关键字,排序。由单次基数排序的稳定性,可知从低关键字往高排若干遍是正确的。
复杂度 \(\mathrm O(\sum(n+v_i))\),其中 \(v_i\) 是第 \(i\) 关键字的值域大小。特殊的,用上述设 \(base\) 方法排整数序列的话,复杂度是 \(\mathrm O((n+base)\log_{base}v)\)。不难发现这种基数排序是严格强于桶排的。
基于倍增的后缀排序算法
假如已知 \(a_{i\sim i+2^k-1}\) 的排序结果,该如何求出 \(a_{i\sim i+2^{k+1}-1}\) 的排序结果。这就有点简单了吧,将后一半当作第二关键字,前一半当作第一关键字排序即可。直接 sort
是线性二次对数的,可以改成 gay 排做到线对。
这样的复杂度在 OI 中基本上可以说足够了。有更优的线性做法,被卡毒瘤题了再学吧。
高度数组
定义高度数组(我也不知道为啥叫这个)\(hi_i\) 为 \(sa_{i-1}\) 和 \(sa_i\) 这两个后缀的 lcp。\(i=1\) 时无定义。
求法
引理:\(hi_{rk_i}\geq hi_{rk_{i-1}}-1\)。证明挺简单的吧,就根据直观性显然有,存在一个后缀与后缀 \(i\) 的 lcp 长度 \(\geq hi_{rk_{i-1}}-1\)。结合下面将要说的 LCP Lemma 可知该引理正确性。
根据该引理,显然有一个按 \(i=1\to n\) 枚举的均摊线性的求法。
给一份 uoj 上模板题的代码吧,lg 和 loj 都太逊,连 \(hi\) 都不要求。
性质
LCP Lemma:后缀 \(i,j(rk_i<rk_j)\) 的 lcp 为 \(hi_{rk_i+1\sim rk_j}\) 的 RMinQ。这个也很好证明了吧,根据 \(sa\) 数组的有序性即可轻松得到该 Lemma 的正确性,详细证明留给读者自己思考。
应用
一般就是根据区间是后缀的前缀,按照 \(sa\) 的顺序在 \(hi\) 数组上搞事情吧,或者根据 LCP Lemma 搞事情……其实 SA 这东西套路啥的也是要多刷题才知道的。一般求出数组们之后就可以转化为 DS 向的另一道题了。
有心的同学可能发现了,SA 是严格强于 Z 的?因为 Z 是求后缀 \(1\)(原串)和其他后缀的 lcp,而 SA 是求任意两个后缀的 lcp。
讲一个非常经典且简单的应用吧:本质不同子串个数。
我们考虑遍历 \(sa\) 数组,由于区间是后缀的前缀,每次把当前后缀的不属于之前遍历过的后缀的前缀的前缀给贡献到答案里,是个 so-called 容斥。那么如何算这个重复的呢。显然有重复的前缀集合是一个前缀。那么和 \(sa_{j<i}\) 的重复前缀集合显然大小为 \(j\sim i\) 的那一段 RMQ。它们的并显然是最大的那个,根据单调性有,重复的数量就是 \(hi_i\)。于是答案是 \(\dfrac{n(n+1)}2-\sum hi_i\)。