后缀树与后缀数组

23 篇文章 0 订阅
11 篇文章 0 订阅

后缀树和后缀数组是字符串处理的两大神器,几乎可处理掉一切的字符串处理问题,但是在实际中,后缀数组比后缀树更好写、好调,同时时间上也不差(常数很小),所以后缀数组绝对是OI竞赛之必备神器。

后缀树,实际上就是一棵字典树。考虑将某个串 S 的所有后缀插到一棵Trie里,那么我们就得到了一棵后缀树。在这里,我们不会讲后缀树的构造,只会略微讲一点其的思想。
在后缀树而言,匹配就变成一件易事了。考虑一个字符串匹配问题,如果我们要在一个串A中找很多个字符串 B=b0,b1,b2 ,那么也只需建好 A 的后缀树,然后直接放就好了……
另外,还有许多扩展应用,但在这里我就不细讲了。下面来讲后缀数组。
同样将一个字符串S的所有后缀,按照字典序排序,那么我们就得到了后缀数组。也就是说,后缀数组的第 i 个正是字典序第i大的后缀的编号。
好的,那么怎么构造呢?一种方法是从排序入手,由于字符串的比较是 Θ(n) 的,所以说再乘上排序的比较次数(如使用快排) nlog2n ,那么总的时间复杂度就是 O(n2log2n)
但是,后缀有一些特殊的性质,可以帮我们优化排序。


我们要用的方法,是倍增算法,如果,我们每一次比较的时候,并不比较整个后缀,而是每次只比较一部分
首先,我们将每个后缀的第一个字符排序,做出第一轮的sa(也就是Suffix Array,后缀数组),然后排第二轮,这次排两个字符,相当于是给二元组排序。
然后,我们开始排四个字符,注意到,由于 4=2+2 ,所以实际上……我们如果利用上一轮的排名,比如我们搞出另一个 rank ,使 rank(i) 为第 i 个后缀的排名(可能重叠),然后,假设我们现在要排后缀j的前四个字符,那么我们将这四个字符拆开,我们就可以得到前面的两个字符和后面的两个字符。前面的两个字符的排名正好是 rank(j) ,而后面的两个字符,则是后缀 j+2 的前两个字符,其的排名是 rank(j+2) 。另外,我们来顺便证明一个定理:将若干个长度相同(设都为 k )的字符串中的一个(e)分成两段,长度分别为 l |e|l,那么如果我们已知字符串 e 在所有字符串的前l的字符构成的串中的排名和剩下来 |e|l 个字符构成的串中的排名 r1 r2 ,那么整一个字符串的排名等价于二元组 (r1,r2) 的排名。证明很容易,我们只需要注意到两个二元组 (a,b) (c,d) 的比较规则是当 ac 时比较 a c,而当 a=c 时比较 b d,那么我们就能够看出,由于我们已经有了前面一段的排名,那么我们只需要比较排名,排名哪一个更前,哪一个的字典序就相应的越高;如果其前半段的排名相等,即字典序相等,那么其前半段是完全一样的,我们就可以忽略前半段,比较后半段的排名。仔细一想,我们就可以发现,上面给出的二元组的比较过程,和字典序的比较过程,是完全一样的!只不过,我们“已经比较”过某些东西了,所以不用再直接地比较两两间的字典序了。
利用倍增算法和快速排序的结合,我们可以在 log2n×Θ(nlog2n)=Θ(nlog22n) 的时间内解决这个问题,代码也很短,五六行左右就好了。如果没有时间写其他的话,可以直接用 sort() 和倍增算法相对暴力地解决。
但是,我们有更高效的算法。基数排序!!!


在这里,我们可以先按照上一轮的结果将第二个关键字排成一个序(相同的不管它),然后按照这个顺序,排。注意,插入的顺序应当和我们预先排的顺序一样。也就是说,大概是下面这样:

for (int i=0;i<n;++i){
    insert(num1[i],i);
}

然后我们再将放进去的二元组再按照桶的大小顺序取出来,放好顺序,基数排序就完成了。
但是,光有构造远远不够,这样我们只能拥有两个数组(而且本质上相同) rank sa 。我们还需要一个特殊的工具,这个工具叫做最长公共前缀。


两个字符串 S1 S2 的最长公共前缀 Lcp(S1,S2) 的定义为

Lcp(S1,S2)=maxi=1min{|S1|,|S2|}{S1i|S1i=S2i}

那么,我们可以很容易地发现,其实某两个后缀的最长公共前缀 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) 呢?容易证明,

Lcp(Ssai,Ssai+1)Lcp(Ssai,Ssai1)1

那么我们就可以不断地用上一个的答案来求出一个下界,再暴力扩展,然后继续求出下一个答案了。最后再用RMQ一搞就好了。
后缀数组的应用相当多,而且实现相对简洁(可能吧),只要多想想,几乎没什么是搞不定的……

【P.S.】后缀数组还可以有各种扩展,比如后缀家族的各种数据结构:后缀树、后缀数组、后缀自动机、后缀仙人掌……所以,太多了……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值