后缀数组及其应用
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
后缀树的特点: 后缀树实质上是一棵字典树。从根节点到任意叶子节点 都对应了原串的一个后缀。但与Trie结构不同的是树上的每一 条边记录的不是单个字母,而是一个字符串。这样一来,将 空间从Trie的O(N^2)降到O(N) 。 同时,Trie的构造时间是O(N^2),而后缀树的构造时间仅 为O(N)。这也是后缀树广为人知的原因。 下面我们看个实例:
2、将k每次加倍,时间压缩成logn级。
3、类似于动规中的ST。
讲了这么多,我们来看个实例:
最长公共子串问题。
最长公共子串问题: 给定两个字符串S1[],S2[]。求出他们的最长公共子串。 例如: s1=“she handsome”,s2=“he slim”。则输出字符 串”he”。(N<=10^5) 首先明确一个事实,任何子串都是某一后缀的前缀。那么, 很直观的想法,把两个串接起来,中间用‘$’隔开。对这个新 的字符串求后缀树组,每次求出排名相邻的两个后缀的最长前 缀即可。当然,这个最长前缀不能跨过‘$’。 新问题出现了: 最长公共前缀怎么求?难道一个一个比较,那么时间复杂 度又回到了O(N^2)级了。看来,如果只有sa[],后缀数组还 不够强大。
现在,我们可以很好的解决刚才的最长公共子串问题了, 构造好后缀数组后,答案就是height[i]中的最大值。当然,要 注意判断公共子串不能跨过‘$’。 时间复杂度O(nlogn)。 下面,我们来看另外一个例子:
最长回文子串(ural1297):
给出一个字符串,请输出其最长回文子串。比如: s=“abzrzbaccc”,输出:abzrzba 跟上题类似,一个直观的想法。
即suffix(i)与排名在它前一位的后缀的最长公共前缀长度。 不难得出h[i]的重要性质:
h[i] >= h[i-1] - 1
证明:h[i]>=h[i-1]-1 证: 首先,明确一个基本事实:对于任意的i<=j<k,有LCP(j,k)>LCP(i,k)。直观上 理解相对于同一个后缀,与他排得越近的后缀的最长公共前缀一定更长。 1、若h[i-1]<=1,则h[i]>=0>=h[i-1]-1显然成立。 2、若h[i-1]>1. 带入定义式,即height[rank[i-1]]>1,又因为height[0]=0,所以rank[i-1]>1。 为了简单起见,令j=i-1,k=sa[rank[i-1]-1]。则suffix(k)<suffix(j)。 因为h[i-1]>1,所以LCP(rank[k+1],rank[i])=h[i-1]-1. 又rank[k+1]<rank[i],所以rank[i]-1>=rank[k+1]. 利用开头那个引理, h[i]=LCP(rank[i]-1,rank[i]) >=LCP(rank[k+1],rank[i])=h[i-1]-1. 综上,有h[i]>=h[i-1]-1。
S=“BANANAS”。
BANANAS 的后缀树
BANANAS 的Trie
后缀树在处理字符串问题上有着得天独厚的空间优势和速 度优势,在最坏情况下, 后缀树的节点数也不会超过2N。主 流的构造方法是由Esko Ukkonen 于1995年发明的一种线 性构造法,理论时间复杂度为O(N)。非常优秀。
在此,我向大家介绍后缀数组的强力外援
——LCP(Longest Common Prefix)
定义: LCP(I,J)={suffix(sa[i])与suffix(sa[j])的最长公共前缀长 度。}即排好序的后缀ห้องสมุดไป่ตู้第i名和第j名的最长公共前缀。
重要性质:
LCP(I,J)=min{LCP(K,K-1)} I <= K <= J 现在摆在我们面前的问题变成了如何快速地求出LCP(I,J)。 从LCP的性质可以看出,suffix(i)与suffix(j)的最长公共前缀 实际上是排名位于i与j之间 排名相邻的后缀的最长公共前缀 中最短的那个前缀。 RMQ问题!!! 排名相邻的后缀的最长公共前缀可以预处理出来,那么 查询LCP(I,J)就相当于查询LCP(I+1,I)到LCP(J-1,J)之间的最 小值。
后缀数组的两种主流构造方法
倍增算法(Double Algorithm) O(nlogn)
三分算法(Difference Cover modulo 3 ) O(N)
倍增算法(Double Algorithm)
总体来说,倍增算法的思想与ST的思想差不多。将后缀 长度依次分为1,2,4,8,。。。,2^k进行排序。进行当 前排序时利用到上次的排序结果。
通过上面的例子,通过k-rank[]可以在O(1)的时间内 完成suffix(i)和suffix(j)的比较。这样就充分利用了后缀之 间有机的联系。 具体实现当然不是枚举每两个串进行比较。进一步想 想,这样比较不就是将每个后缀看作一个元素,k-rank[i]作 为第一关键字,k-rank[i+k]作为第二关键字进行排序吗? 这时,排序方法又是一个大问题。虽然快排等比较型排序 可以做到O(nlogn),但是还不能满足后缀树组的高效要求。 每个元素只有两个关键字,而且关键字分布比较集中,相信聪 明的你已经想到了:
首先给出一些新的定义: 1、字符串均以“000000….”结尾,‘0’为最 小字符; 2、定义k-rank[],即在比较每个后缀前k个字符 的情况下,suffix(i)的名次记做k-rank[i].类似地定义 k-sa[]。
当前求2k-rank[],2k-sa[]。
K个字符 K个字符 K个字符 K个字符
在现今的信息学竞赛中后缀数组在字符串处理方面有着 良好的口碑,许多字符串难题只要构造出后缀数组就能迎刃 而解。 它无论从时间还是空间上都很好的代替了后缀树的 作用。让我们一起来看看它的一些基本定义: 规定: n = length(s),即n为字符串的长度 s[i…j]={字符串s的第i个到第j个}
suffix(i) = s[i…n],即字符串s的第i个后缀
有了这个性质,我们就可以根据h[1],h[2],…..,h[n]顺序先 计算出h[i]。再根据height[i]=h[sa[i]]计算出height[]数组。
在h[i]时,由于suffix(i)与suffix(sa[rank[i]-1])的前 h[i-1]-1个字符已经相同了,所以最多只需要h[i]-h[i-1]+2次 比较就能找到某两个对应字符不相同。所以总的时间复杂度最 多不会超过4n次,算法是线性的, 算法总流程
缺点: 该算法实现过于复杂,细节较多,且代码量较长,对于 一名信息竞赛的选手而言,在短短的5个小时内,要完全正 确地写出后缀树谈何容易。在各种各样的比赛中,我们要 选择实现相对简单,且效率较高的算法和数据结构。
随着广大OIERS的呼声,
后缀数组横空出世!!
*后缀数组 (Suffix Arrary)
两个重要元素
将字符串的n个后缀按照字典序从小到大排序,我 们把suffix(i)的名次记做rank[i],将排好序的n个后缀的 开头位置依次按照从小到大的顺序存入sa[]中,即 sa[i]=第i小的后缀的编号。举例:
S=“AABACABA” S Rank Sa A 2 8 A 4 1 B 7 6 A 5 2 C 8 4 A 3 7 B 6 3 A 1 5
现在只需求出1-rank[]和1-sa[]就可以每次通过O(n)的排 序转移。至于求1-rank[]与1-sa[],把原串中的字符排序即可, 快排或基数排序都可以,均不影响算法的时间复杂度。但是建 议使用基数排序。 O(n)排序 原始串S 1-rank[],1-sa[] O(n)排序 L-rank[],l-sa[].
后缀数组及 其应用
本文探讨内容:
1、后缀树组的概念及构造方 法; 2、后缀树组的相关应用;
有关后缀树(Suffix Tree ):
提到后缀数组,我们不由自主地会想到后缀树。后缀树 (Suffix tree)是一种数据结构,能快速解决很多关于字符 串的问题。后缀树的概念最早由Weiner 于1973年提出,既 而由McCreight 在1976年和Ukkonen在1992年和1995年加 以改进完善。
关于RMQ问题(Range Minimum Query)
线段树等高级数据结构维护,O(nlogn)构造,O(logn) 查询,ST算法O(nlogn)构造,O(1)的查询。RMQ标准算法 O(n)构造,0(1)查询。
在信息竞赛中,均衡利弊还是ST实现简单,效率较高, 性价比高。 这样一来,我们可以在O(1)的时间内查询任意两个后缀的 最长公共前缀。这也是后缀数组最强大的功能之一。 貌似问题得到了解决,回顾刚才的过程,我们漏掉了一 个重要的过程——预处理排名相邻后缀 暴力枚举后缀再比较。时间显然超过了O(n)。我们希 望能在O(n)时间内解决这一问题
O(N)
Height[]数组及其高效计算
还是先看定义:
height[i] = LCP(sa[i-1],sa[i])
即排名相邻的两个后缀的最长公共前缀长度。 对于height[]数组的计算,我们并不能顺序递推。需要充 分利用字符串之间的联系,改变他的计算顺序。 所以,我们定义h[i]:
h[i] = height[rank[i]]
后缀数组的构造方法:
比较直观的想法——暴力排序: 把n个后缀预处理出来,快排,堆排,归并等,时 间复杂度O(nlogn),看上去还不错。 但仔细想想,两个字符串间比较有个strcmp(),这 明显不是线性。况且kmp也需要O(N+M)的时间,这样 一来时间复杂度接近O(n^2)。 当N>10000时就悲剧了。。。。。
接着将筒中的数依次倒出,得到: 81, 22, 73, 93, 43, 14, 55, 65, 28, 39 再按照十位数依次放入筒中. 0 1 14 2 22 28 3 39 4 43 5 55 6 7 8 81 9 93
65 73
按顺序倒出后,得到排好序的序列: 14,22,28,39,43,55,65,73,81,93 基数排序的时间复杂度是时间复杂度可以为O(n)。
基数排序(Radix Sort)!!
基数排序(Radix Sort)
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排 序方式由键值的最右边开始,而MSD则相反,由键值的最左 边开始。
以LSD为例,先有一串数:73, 22, 93, 43, 55, 14, 28, 65, 39, 81 按照他们的原始顺序,将个位分离出来,放入筒中。每个 桶按照队列方式存储(先进先出) 0 1 81 2 22 3 73 93 43 4 14 5 55 65 6 7 8 28 9 39
其中l=2^k,且 l>=n
O(n)排序 。。。 。。。
2-rank[]
2-sa[]
8-rank[] 8-sa[] O(n)排序 4-rank[] 4-sa[] O(n)排序
算法最多进行logn次,每次 时间为O(N).所以最终时间 复杂度为O(nlogn)
倍增算法——思想总结
1、定义k-rank[],并利用k-rank[]求出2k-rank[],高 效的比较;充分利用了后缀之间的联系;
suffix(i) 从上图可以看出:
Suffix(j)
比较suffix(i)和suffix(j)只需先比较红色部分,再比较 绿色部分。而由于k-rank[]是已知的。所以有以下结论:
2k-rank[i] > 2k-rank[j]
k-rank[i] > k-rank[j] 或 k-rank[i] == k-rank[j] && k-rank[i+k] > k-rank[j+k]
O(nlogn)
O(n)。
1、double_algorithm构造后缀数组;。。。。。O(nlogn) 2、线性计算出h[]数组,再逐个推出height[i];。。。O(n) 3、ST算法对height[]做预处理;。。。。。。。O(nlogn) 4、查询LCP(I,J)只需查询height[i…j]中的最小值 O(1)
2、将k每次加倍,时间压缩成logn级。
3、类似于动规中的ST。
讲了这么多,我们来看个实例:
最长公共子串问题。
最长公共子串问题: 给定两个字符串S1[],S2[]。求出他们的最长公共子串。 例如: s1=“she handsome”,s2=“he slim”。则输出字符 串”he”。(N<=10^5) 首先明确一个事实,任何子串都是某一后缀的前缀。那么, 很直观的想法,把两个串接起来,中间用‘$’隔开。对这个新 的字符串求后缀树组,每次求出排名相邻的两个后缀的最长前 缀即可。当然,这个最长前缀不能跨过‘$’。 新问题出现了: 最长公共前缀怎么求?难道一个一个比较,那么时间复杂 度又回到了O(N^2)级了。看来,如果只有sa[],后缀数组还 不够强大。
现在,我们可以很好的解决刚才的最长公共子串问题了, 构造好后缀数组后,答案就是height[i]中的最大值。当然,要 注意判断公共子串不能跨过‘$’。 时间复杂度O(nlogn)。 下面,我们来看另外一个例子:
最长回文子串(ural1297):
给出一个字符串,请输出其最长回文子串。比如: s=“abzrzbaccc”,输出:abzrzba 跟上题类似,一个直观的想法。
即suffix(i)与排名在它前一位的后缀的最长公共前缀长度。 不难得出h[i]的重要性质:
h[i] >= h[i-1] - 1
证明:h[i]>=h[i-1]-1 证: 首先,明确一个基本事实:对于任意的i<=j<k,有LCP(j,k)>LCP(i,k)。直观上 理解相对于同一个后缀,与他排得越近的后缀的最长公共前缀一定更长。 1、若h[i-1]<=1,则h[i]>=0>=h[i-1]-1显然成立。 2、若h[i-1]>1. 带入定义式,即height[rank[i-1]]>1,又因为height[0]=0,所以rank[i-1]>1。 为了简单起见,令j=i-1,k=sa[rank[i-1]-1]。则suffix(k)<suffix(j)。 因为h[i-1]>1,所以LCP(rank[k+1],rank[i])=h[i-1]-1. 又rank[k+1]<rank[i],所以rank[i]-1>=rank[k+1]. 利用开头那个引理, h[i]=LCP(rank[i]-1,rank[i]) >=LCP(rank[k+1],rank[i])=h[i-1]-1. 综上,有h[i]>=h[i-1]-1。
S=“BANANAS”。
BANANAS 的后缀树
BANANAS 的Trie
后缀树在处理字符串问题上有着得天独厚的空间优势和速 度优势,在最坏情况下, 后缀树的节点数也不会超过2N。主 流的构造方法是由Esko Ukkonen 于1995年发明的一种线 性构造法,理论时间复杂度为O(N)。非常优秀。
在此,我向大家介绍后缀数组的强力外援
——LCP(Longest Common Prefix)
定义: LCP(I,J)={suffix(sa[i])与suffix(sa[j])的最长公共前缀长 度。}即排好序的后缀ห้องสมุดไป่ตู้第i名和第j名的最长公共前缀。
重要性质:
LCP(I,J)=min{LCP(K,K-1)} I <= K <= J 现在摆在我们面前的问题变成了如何快速地求出LCP(I,J)。 从LCP的性质可以看出,suffix(i)与suffix(j)的最长公共前缀 实际上是排名位于i与j之间 排名相邻的后缀的最长公共前缀 中最短的那个前缀。 RMQ问题!!! 排名相邻的后缀的最长公共前缀可以预处理出来,那么 查询LCP(I,J)就相当于查询LCP(I+1,I)到LCP(J-1,J)之间的最 小值。
后缀数组的两种主流构造方法
倍增算法(Double Algorithm) O(nlogn)
三分算法(Difference Cover modulo 3 ) O(N)
倍增算法(Double Algorithm)
总体来说,倍增算法的思想与ST的思想差不多。将后缀 长度依次分为1,2,4,8,。。。,2^k进行排序。进行当 前排序时利用到上次的排序结果。
通过上面的例子,通过k-rank[]可以在O(1)的时间内 完成suffix(i)和suffix(j)的比较。这样就充分利用了后缀之 间有机的联系。 具体实现当然不是枚举每两个串进行比较。进一步想 想,这样比较不就是将每个后缀看作一个元素,k-rank[i]作 为第一关键字,k-rank[i+k]作为第二关键字进行排序吗? 这时,排序方法又是一个大问题。虽然快排等比较型排序 可以做到O(nlogn),但是还不能满足后缀树组的高效要求。 每个元素只有两个关键字,而且关键字分布比较集中,相信聪 明的你已经想到了:
首先给出一些新的定义: 1、字符串均以“000000….”结尾,‘0’为最 小字符; 2、定义k-rank[],即在比较每个后缀前k个字符 的情况下,suffix(i)的名次记做k-rank[i].类似地定义 k-sa[]。
当前求2k-rank[],2k-sa[]。
K个字符 K个字符 K个字符 K个字符
在现今的信息学竞赛中后缀数组在字符串处理方面有着 良好的口碑,许多字符串难题只要构造出后缀数组就能迎刃 而解。 它无论从时间还是空间上都很好的代替了后缀树的 作用。让我们一起来看看它的一些基本定义: 规定: n = length(s),即n为字符串的长度 s[i…j]={字符串s的第i个到第j个}
suffix(i) = s[i…n],即字符串s的第i个后缀
有了这个性质,我们就可以根据h[1],h[2],…..,h[n]顺序先 计算出h[i]。再根据height[i]=h[sa[i]]计算出height[]数组。
在h[i]时,由于suffix(i)与suffix(sa[rank[i]-1])的前 h[i-1]-1个字符已经相同了,所以最多只需要h[i]-h[i-1]+2次 比较就能找到某两个对应字符不相同。所以总的时间复杂度最 多不会超过4n次,算法是线性的, 算法总流程
缺点: 该算法实现过于复杂,细节较多,且代码量较长,对于 一名信息竞赛的选手而言,在短短的5个小时内,要完全正 确地写出后缀树谈何容易。在各种各样的比赛中,我们要 选择实现相对简单,且效率较高的算法和数据结构。
随着广大OIERS的呼声,
后缀数组横空出世!!
*后缀数组 (Suffix Arrary)
两个重要元素
将字符串的n个后缀按照字典序从小到大排序,我 们把suffix(i)的名次记做rank[i],将排好序的n个后缀的 开头位置依次按照从小到大的顺序存入sa[]中,即 sa[i]=第i小的后缀的编号。举例:
S=“AABACABA” S Rank Sa A 2 8 A 4 1 B 7 6 A 5 2 C 8 4 A 3 7 B 6 3 A 1 5
现在只需求出1-rank[]和1-sa[]就可以每次通过O(n)的排 序转移。至于求1-rank[]与1-sa[],把原串中的字符排序即可, 快排或基数排序都可以,均不影响算法的时间复杂度。但是建 议使用基数排序。 O(n)排序 原始串S 1-rank[],1-sa[] O(n)排序 L-rank[],l-sa[].
后缀数组及 其应用
本文探讨内容:
1、后缀树组的概念及构造方 法; 2、后缀树组的相关应用;
有关后缀树(Suffix Tree ):
提到后缀数组,我们不由自主地会想到后缀树。后缀树 (Suffix tree)是一种数据结构,能快速解决很多关于字符 串的问题。后缀树的概念最早由Weiner 于1973年提出,既 而由McCreight 在1976年和Ukkonen在1992年和1995年加 以改进完善。
关于RMQ问题(Range Minimum Query)
线段树等高级数据结构维护,O(nlogn)构造,O(logn) 查询,ST算法O(nlogn)构造,O(1)的查询。RMQ标准算法 O(n)构造,0(1)查询。
在信息竞赛中,均衡利弊还是ST实现简单,效率较高, 性价比高。 这样一来,我们可以在O(1)的时间内查询任意两个后缀的 最长公共前缀。这也是后缀数组最强大的功能之一。 貌似问题得到了解决,回顾刚才的过程,我们漏掉了一 个重要的过程——预处理排名相邻后缀 暴力枚举后缀再比较。时间显然超过了O(n)。我们希 望能在O(n)时间内解决这一问题
O(N)
Height[]数组及其高效计算
还是先看定义:
height[i] = LCP(sa[i-1],sa[i])
即排名相邻的两个后缀的最长公共前缀长度。 对于height[]数组的计算,我们并不能顺序递推。需要充 分利用字符串之间的联系,改变他的计算顺序。 所以,我们定义h[i]:
h[i] = height[rank[i]]
后缀数组的构造方法:
比较直观的想法——暴力排序: 把n个后缀预处理出来,快排,堆排,归并等,时 间复杂度O(nlogn),看上去还不错。 但仔细想想,两个字符串间比较有个strcmp(),这 明显不是线性。况且kmp也需要O(N+M)的时间,这样 一来时间复杂度接近O(n^2)。 当N>10000时就悲剧了。。。。。
接着将筒中的数依次倒出,得到: 81, 22, 73, 93, 43, 14, 55, 65, 28, 39 再按照十位数依次放入筒中. 0 1 14 2 22 28 3 39 4 43 5 55 6 7 8 81 9 93
65 73
按顺序倒出后,得到排好序的序列: 14,22,28,39,43,55,65,73,81,93 基数排序的时间复杂度是时间复杂度可以为O(n)。
基数排序(Radix Sort)!!
基数排序(Radix Sort)
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排 序方式由键值的最右边开始,而MSD则相反,由键值的最左 边开始。
以LSD为例,先有一串数:73, 22, 93, 43, 55, 14, 28, 65, 39, 81 按照他们的原始顺序,将个位分离出来,放入筒中。每个 桶按照队列方式存储(先进先出) 0 1 81 2 22 3 73 93 43 4 14 5 55 65 6 7 8 28 9 39
其中l=2^k,且 l>=n
O(n)排序 。。。 。。。
2-rank[]
2-sa[]
8-rank[] 8-sa[] O(n)排序 4-rank[] 4-sa[] O(n)排序
算法最多进行logn次,每次 时间为O(N).所以最终时间 复杂度为O(nlogn)
倍增算法——思想总结
1、定义k-rank[],并利用k-rank[]求出2k-rank[],高 效的比较;充分利用了后缀之间的联系;
suffix(i) 从上图可以看出:
Suffix(j)
比较suffix(i)和suffix(j)只需先比较红色部分,再比较 绿色部分。而由于k-rank[]是已知的。所以有以下结论:
2k-rank[i] > 2k-rank[j]
k-rank[i] > k-rank[j] 或 k-rank[i] == k-rank[j] && k-rank[i+k] > k-rank[j+k]
O(nlogn)
O(n)。
1、double_algorithm构造后缀数组;。。。。。O(nlogn) 2、线性计算出h[]数组,再逐个推出height[i];。。。O(n) 3、ST算法对height[]做预处理;。。。。。。。O(nlogn) 4、查询LCP(I,J)只需查询height[i…j]中的最小值 O(1)