后缀树和后缀数组是字符串处理的两大神器,几乎可处理掉一切的字符串处理问题,但是在实际中,后缀数组比后缀树更好写、好调,同时时间上也不差(常数很小),所以后缀数组绝对是OI竞赛之必备神器。
后缀树,实际上就是一棵字典树。考虑将某个串
S
的所有后缀插到一棵Trie里,那么我们就得到了一棵后缀树。在这里,我们不会讲后缀树的构造,只会略微讲一点其的思想。
在后缀树而言,匹配就变成一件易事了。考虑一个字符串匹配问题,如果我们要在一个串
另外,还有许多扩展应用,但在这里我就不细讲了。下面来讲后缀数组。
同样将一个字符串
好的,那么怎么构造呢?一种方法是从排序入手,由于字符串的比较是
Θ(n)
的,所以说再乘上排序的比较次数(如使用快排)
nlog2n
,那么总的时间复杂度就是
O(n2log2n)
。
但是,后缀有一些特殊的性质,可以帮我们优化排序。
我们要用的方法,是倍增算法,如果,我们每一次比较的时候,并不比较整个后缀,而是每次只比较一部分。
首先,我们将每个后缀的第一个字符排序,做出第一轮的sa(也就是Suffix Array,后缀数组),然后排第二轮,这次排两个字符,相当于是给二元组排序。
然后,我们开始排四个字符,注意到,由于
4=2+2
,所以实际上……我们如果利用上一轮的排名,比如我们搞出另一个
rank
,使
rank(i)
为第
i
个后缀的排名(可能重叠),然后,假设我们现在要排后缀
利用倍增算法和快速排序的结合,我们可以在
log2n×Θ(nlog2n)=Θ(nlog22n)
的时间内解决这个问题,代码也很短,五六行左右就好了。如果没有时间写其他的话,可以直接用
sort()
和倍增算法相对暴力地解决。
但是,我们有更高效的算法。基数排序!!!
在这里,我们可以先按照上一轮的结果将第二个关键字排成一个序(相同的不管它),然后按照这个顺序,桶排。注意,插入的顺序应当和我们预先排的顺序一样。也就是说,大概是下面这样:
for (int i=0;i<n;++i){
insert(num1[i],i);
}
然后我们再将放进去的二元组再按照桶的大小顺序取出来,放好顺序,基数排序就完成了。
但是,光有构造远远不够,这样我们只能拥有两个数组(而且本质上相同)
rank
和
sa
。我们还需要一个特殊的工具,这个工具叫做最长公共前缀。
两个字符串 S1 和 S2 的最长公共前缀 Lcp(S1,S2) 的定义为
那么,我们可以很容易地发现,其实某两个后缀的最长公共前缀
Lcp(Ssai,Ssaj)
的值,实际上是
mink=min{sai,saj}max{sai,saj}−1Lcp(Sk,Sk+1)
。证明很容易,首先显然地,
mink=min{sai,saj}max{sai,saj}−1Lcp(Sk,Sk+1)≤Lcp(Ssai,Ssaj)
(相当于若干个公共前缀的公共部分,可能可以再长),另外若设
p=mink=min{sai,saj}max{sai,saj}−1Lcp(Sk,Sk+1)
,
q=Lcp(Ssai,Ssaj)
,而
q>p
,那么可以推出矛盾,这里不证(篇幅太长了,公式太大了……)
好吧,总之这个被证出来了,那么我们怎样快速地求出
Lcp(Ssai,Ssai+1)
呢?容易证明,
那么我们就可以不断地用上一个的答案来求出一个下界,再暴力扩展,然后继续求出下一个答案了。最后再用RMQ一搞就好了。
后缀数组的应用相当多,而且实现相对简洁(可能吧),只要多想想,几乎没什么是搞不定的……
【P.S.】后缀数组还可以有各种扩展,比如后缀家族的各种数据结构:后缀树、后缀数组、后缀自动机、后缀仙人掌……所以,太多了……