代码改变世界

实用算法实现-第8篇 后缀树和后缀数组 [1简介]

2011-10-15 11:50  myjava2  阅读(754)  评论(0编辑  收藏  举报

8.1    后缀树

一棵后缀树包含一个指定文本的所有后缀,对于在一个长度为N的文本中查找一个长度为M的子字符串,一个后缀树仅仅需要M次比较,而这个比较次数是查找该字符串所需要的最小比较次数。

后缀树有以下特征:一条边可以表示文本的任何非空子串,每个非终端节点,除了根节点,必须至少有两个孩子边,兄弟边表示的子串必须开始于不同的字符。

ababa所对应的后缀树如下所示:

后缀树可以解决很多复杂的字符串编程问题,包括:

1. 查询字符串S是否包含子串S1。主要思想是:如果S包含S1,那么S1必定是S的某个后缀的前缀;又因为S的后缀树包含了所有的后缀,所以只需对S的后缀树使用和Trie相同的查找方法查找S1即可。使用后缀树实现的复杂度同流行的KMP算法的复杂度相当。

2. 找出字符串S的最长重复子串S1。比如abcdabcefda里abc同da都重复出现,而最长重复子串是abc。

3. 找出字符串S1和S2的最长公共子串。注意最长公共子串(Longest CommonSubstring)和最长公共子序列(LongestCommon Subsequence, LCS)的区别:子串(Substring)是串的一个连续的部分,子序列(Subsequence)则是从不改变序列的顺序,而从序列中去掉任意的元素而获得的新序列;更简略地说,前者的字符的位置必须连续,后者则不必。比如字符串acdfg同akdfc的最长公共子串为df,而他们的最长公共子序列是adf。LCS可以使用动态规划法解决。

4. Lempel-Ziv-Welch (LZW)无损压缩算法。LZW算法的基本原理是利用编码数据本身存在字符串重复特性来实现数据压缩,所以一个很好的选择是使用后缀树的形式来组织存储字符串及其对应压缩码值的字典。在UNIX COMPRESS程序中使用的LZW压缩的源程序可以在[i]找到。

5. 找出字符串S的最长回文子串S1。例如:XMADAMYX的最长回文子串是MADAM。

6. 多模式串的模式匹配问题。(suffer_array+二分)。

后缀树的构造可以用Ukkonen算法在线性时间内完成[ii],但是不仅构造算法实现相当复杂,而且后缀树存在着致命弱点:空间开销大且对大字母表时间效率不理想[iii]

8.2    后缀数组

使用后缀数组可以避免使用后缀树带来的诸多不便。不仅可以通过使用Skew算法[iv](该文发在会议上,发在期刊上文章[v]将其称为Difference Cover mod 3算法,即DC3算法)在线性时间内完成对后缀数组的构造,而且可以证明:后缀树上的任意算法都可以用增强的后缀数组来实现[vi]

后缀树到后缀数组的转化,可以通过深度优先(按照字符串的排序顺序搜索子结点)遍历来完成。而后缀数组则除了保留排序结果之外,相对于后缀树则缺失了很多信息,因此需要对之进行扩展。

       一个最常用的扩展是引入高度数组height。

定义height为:

Height[i] = LCP(SA[i - 1] , SA[i]) 当i > 1

Height[i] = 0当i = 1

LCP是Longest Common Prefix的缩写,即最长公共前缀,表示两个串从第一个字符开始的相等的连续字符串的长度。

定义rank[i]为后缀i在SA中的位置。

定义h[i] = Height[rank[i]]。

对后缀树进行一些改造:按照子结点的字符串排序顺序,将这个后缀树的子结点按照从左到右进行排列。那么后缀树到后缀数组的转化即可自然地左优先(即优先搜索最左边的子树)的深度优先遍历来完成。后缀数组的每个元素都对应着后缀树的叶子节点。如果SA[i]对应了后缀树的一个叶结点N[i],SA[i - 1]在后缀树中就对应叶结点N[i - 1]。可知SA[i]在后缀数组中的左邻是SA[i - 1],N[i]在后缀树中的左邻叶结点是N[i - 1]。N[i]和N[i - 1]的最近公共祖先为LCA(N[i], N[i - 1])。从树根到LCA(N[i], N[i - 1])所经过的字符串为SA [i - 1] 和SA [i]的最长公共前缀,其长度为LCP(SA[i - 1] , SA[i])。

height[i]的意义是SA[i - 1]和SA[i]的公共前缀的长度,也就是在后缀树中对应的两个叶结点的最近公共祖先对应的字符串的长度,在不压缩的Trie形式的后缀树中(称这种树为后缀Trie树)就是LCA的深度。

引理:当 i > 0 且 rank[i] > 0 时, h[i] ≥ h[i - 1] - 1

证明:用反证法。

令字符串S从i开始的后缀为Suffix[i]。那么h[i]和h[i-1]分别考察的是Suffix [i]和Suffix [i - 1]与它们左邻后缀的LCA的长度。从后缀Trie树的根节点遍历到Suffix [i]对应的叶子结点,在这个过程中所经过的结点、分支、对分支的选择(即把第几个分支作为遍历的子结点)确定了Suffix [i]的遍历路径。由于Suffix[i]的前面加上字符串S的第i - 1个字符S[i - 1]就等于Suffix[i - 1],所以Suffix [i - 1]的遍历路径在经过第一个结点(该结点就是字符S[i - 1] 对应的结点)之后,就已经和Suffix [i]的遍历路径一致了。

假设h[i] <h[i - 1] - 1,那么Suffix [i -1]经过h[i - 1]个结点之后到达其LCA结点N,Suffix [i]的遍历路径在经过h[i - 1] - 1个结点之后已经超过其LCA,并到达结点N’。 N至少含有两个分支:一个是Suffix[i - 1]对应叶结点的分支,一个是该叶结点的左邻叶结点的分支。由于遍历路径的一致性,N’也至少至少含有两个分支:其中一个是Suffix[i]对应叶结点的分支,另一个分支在该分支的左边。由于每个分支至少含有一个叶结点,故此存在着一个叶结点处于Suffix [i]对应叶结点和该结点的左邻叶结点之间。这与左邻的含义矛盾。

证毕。

将LCP(SA(i),SA(j))简化表示为LCP(i, j)。

LCP定理:设i < j,则LCP(i, j) = min{LCP(k - 1, k)|i +1 ≤ k ≤ j}

可知LCP(k - 1,k)即为Height[k]。

这是一个RMQ问题。RMQ问题可以在线性的预处理时间后支持O(1)的LCP询问。


本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article

[i] http://www-igm.univ-mlv.fr/~mac/ENS/DOC/COMP/

[ii] On-line Construction of Suffix Trees. Esko Ukkonen.

[iii] 算法艺术与信息学竞赛学习指导。刘汝佳,周源,周戈林。

[iv] Simple Linear Work Suffix Array Construction. Juha Kärkkäinen,Peter Sanders. In Proc. 30th International Colloquium on Automata, Languagesand Programming (ICALP '03). LNCS 2719, Springer, 2003, pp. 943-955.

[v] Linear Work Suffx Array Construction. Juha Kärkkäinen, PeterSandersy, Stefan Burkhardtz. J. ACM, 53 (6), pp. 918-936, 2006.

[vi] Replacing Suffix Trees with Enhanced Suffix Arrays. Mohamed IbrahimAbouelhoda, Stefan Kurtz, Enno Ohlebusch.