后缀数组及其应用
【字符串】后缀数组
【字符串】后缀数组后缀排序倍增算法n字符串的长度。
m当前后缀(离散化后)的值域。
对于char可以跳过离散化,初值取128即可,对于int要离散化,初值取n即可,初值要保证覆盖整个值域。
sa[i]排名为i的后缀的起始位置。
rk[i]起始位置为i的后缀的排名。
验证:const int MAXN = 1000000 + 10;int n, m, ct[MAXN], tp[MAXN];int sa[MAXN], rk[MAXN], ht[MAXN];void RadixSort() {for(int i = 0; i <= m; ++i)ct[i] = 0;for(int i = 1; i <= n; ++i)++ct[rk[i]];for(int i = 1; i <= m; ++i)ct[i] += ct[i - 1];for(int i = n; i >= 1; --i)sa[ct[rk[tp[i]]]--] = tp[i];}bool Compare(int i, int j, int l) {if(tp[sa[i]] == tp[sa[j]]) {if(sa[i] + l <= n && sa[j] + l <= n) {if(tp[sa[i] + l] == tp[sa[j] + l])return 1;}}return 0;}void SuffixSort(char *s) {n = strlen(s + 1), m = 128;for(int i = 1; i <= n; ++i) {rk[i] = s[i];tp[i] = i;}RadixSort();for(int l = 1;; l <<= 1) {m = 0;for(int i = n - l + 1; i <= n; ++i)tp[++m] = i;for(int i = 1; i <= n; ++i) {if(sa[i] > l)tp[++m] = sa[i] - l;}RadixSort();swap(tp, rk);m = 1;rk[sa[1]] = 1;for(int i = 2; i <= n; ++i) {if(Compare(i - 1, i, l) == 0)++m;rk[sa[i]] = m;}if(m == n)break;}}最⼩循环表⽰把字符串S循环移动,找字典序最⼩的那个表⽰。
后缀数组最详细讲解
后缀数组最详细讲解转载⾃后缀数组最详细(maybe)讲解后缀数组这个东西真的是神仙操作……但是这个⽐较神仙的东西在⽹上的讲解⼀般都仅限于思想⽽不是代码,⽽且这个东西开⼀堆数组,很多初学者写代码的时候很容易发⽣歧义理解,所以这⾥给出⼀个⽐较详细的讲解。
笔者⾃⼰也是和后缀数组硬刚了⼀个上午外加⼀个中午才理解的板⼦。
本⼈版权意识薄弱,如有侵权现象请联系博主邮箱xmzl200201@参考⽂献:以下是不认识的dalao们:特别感谢以下的两位dalao,写的特别好,打call什么是后缀数组我们先看⼏条定义:⼦串在字符串s中,取任意i<=j,那么在s中截取从i到j的这⼀段就叫做s的⼀个⼦串后缀后缀就是从字符串的某个位置i到字符串末尾的⼦串,我们定义以s的第i个字符为第⼀个元素的后缀为suff(i)后缀数组把s的每个后缀按照字典序排序,后缀数组sa[i]就表⽰排名为i的后缀的起始位置的下标⽽它的映射数组rk[i]就表⽰起始位置的下标为i的后缀的排名简单来说,sa表⽰排名为i的是啥,rk表⽰第i个的排名是啥⼀定要记牢这些数组的意思,后⾯看代码的时候如果记不牢的话就绝对看不懂后缀数组的思想先说最暴⼒的情况,快排(n log n)每个后缀,但是这是字符串,所以⽐较任意两个后缀的复杂度其实是O(n),这样⼀来就是接近O(n^2 log n)的复杂度,数据⼤了肯定是不⾏的,所以我们这⾥有两个优化。
ps:本⽂中的^表⽰平⽅⽽不是异或倍增⾸先读⼊字符串之后我们现根据单个字符排序,当然也可以理解为先按照每个后缀的第⼀个字符排序。
对于每个字符,我们按照字典序给⼀个排名(当然可以并列),这⾥称作关键字。
接下来我们再把相邻的两个关键字合并到⼀起,就相当于根据每⼀个后缀的前两个字符进⾏排序。
想想看,这样就是以第⼀个字符(也就是⾃⼰本⾝)的排名为第⼀关键字,以第⼆个字符的排名为第⼆关键字,把组成的新数排完序之后再次标号。
没有第⼆关键字的补零。
后缀树与后缀数组
• 显然, LCP(Suffix(i+1), Suffix(j+1)) = max(h[k]-1,0);
i i+1
j j+1
• 设i+1在sa中位置为t,sa[t+1] = p 即h[t] = LCP(suffix(i+1),suffix(p)) • 由suffix(i) < suffix (j) => suffix(i+1) < suffix(j+1) • 而suffix(p) 在sa数组中的位置紧贴着suffix(i+1),所以有 suffix(i+1) < suffix(p) <= suffix(j+1) • 而LCP(suffix(i+1),suffix(j+1)) = max(h[k]-1,0) 下标:1 2 3 4 5 Sa数组
– 当i超过字符串的长度,可以认为s[i] = -oo。
• 后缀:指从某个位置i开始到整个串结束的一个 特殊子串。字符串S的从i个字符开始的后缀记为 Suffix(i)。
– 显然,Suffix(i) = S[i..len(S)],记为S(i)
• 字符串的大小比较:例如串S与串T,从小到大 枚举i,如果s[i] < t[i] => S < T, 如果s[i] > t[i] => S > T。 两个串完全匹配则S== T
• 名次数组:名次数组Rank[i]保存的是 Suffix(i) 在所有后缀中从小到大排 列的“名次”。 可以视为大小
– 简单来说,名次数组就是问“你排第几”
• 显然,两者只要知道一个,就可以推出另外 一个
下标:1
2
后缀数组的应用
二、后缀数组的应用本节主要介绍后缀数组在各种类型的字符串问题中的应用。
各题的原题请见附件二,参考代码请见附件三。
2.1最长公共前缀这里先介绍后缀数组的一些性质。
height数组:定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。
那么对于j和k,不妨设rank[j]<rank[k],则有以下性质:suffix(j)和suffix(k)的最长公共前缀为height[rank[j]+1], height[rank[j]+2],height[rank[j]+3],…,height[rank[k]]中的最小值。
例如,字符串为“aabaaaab”,求后缀“abaaaab”和后缀“aaab”的最长公共前缀,如图4所示:那么应该如何高效的求出height值呢?如果按height[2],height[3],……,height[n]的顺序计算,最坏情况下时间复杂度为O(n2)。
这样做并没有利用字符串的性质。
定义h[i]=height[rank[i]],也就是suffix(i)和在它前一名的后缀的最长公共前缀。
h数组有以下性质:h[i]≥h[i-1]-1证明:设suffix(k)是排在suffix(i-1)前一名的后缀,则它们的最长公共前缀是h[i-1]。
那么suffix(k+1)将排在suffix(i)的前面(这里要求h[i-1]>1,如果h[i-1]≤1,原式显然成立)并且suffix(k+1)和suffix(i)的最长公共前缀是h[i-1]-1,所以suffix(i)和在它前一名的后缀的最长公共前缀至少是h[i-1]-1。
按照h[1],h[2],……,h[n]的顺序计算,并利用h数组的性质,时间复杂度可以降为O(n)。
具体实现:实现的时候其实没有必要保存h数组,只须按照h[1],h[2],……,h[n]的顺序计算即可。
数据结构编译原理 前缀和后缀
数据结构编译原理前缀和后缀前缀和和后缀分别是一种常用的数据结构和算法,它们在编译原理和其他一些计算机科学领域中都有广泛的应用。
前缀和是一种用来统计数组前缀和区间和的算法,也可以叫做累加和。
它的实现方式是先将原数组中的值依次相加得到一个新数组,然后在新数组中查询任意区间的和时,只需要进行一次减法操作即可得到区间和。
例如,给定一个数组arr,前缀和数组prefixSum 的计算方式如下所示:prefixSum[0] = arr[0];for(int i = 1; i < n; i++){prefixSum[i] = prefixSum[i-1] + arr[i];}其中,n是数组的长度。
例如,如果arr=[1,2,3,4,5],则prefixSum=[1,3,6,10,15]。
要查询arr[1]到arr[3]的区间和,只需要进行一次减法,即prefixSum[3]-prefixSum[0]=10-1=9。
前缀和的时间复杂度为O(n),空间复杂度也为O(n),适用于数组多次查询区间和的情况。
需要注意的是,在前缀和计算过程中,可能会出现整型溢出的情况,因此需要根据具体情况选择使用long long等数据类型来避免这种问题。
后缀数组是一种用来快速匹配字符串模式的算法。
它的实现方式是将原字符串的所有后缀按照字典序排序,然后记录每个后缀在排序后的后缀数组中的位置。
例如,给定一个字符串str,后缀数组suffixArray的计算方式如下所示:例如,如果str="banana",则suffix=["banana","anana","nana","ana","na","a"],suffixArray=[5,3,1,0,4,2]。
可以通过对suffixArray数组进行二分查找来快速匹配包含某个字符串模式的后缀。
后缀数组
后缀数组安徽省芜湖市第一中学许智磊【摘要】本文介绍后缀数组的基本概念、方法以及应用。
首先介绍O(nlogn)复杂度构造后缀数组的倍增算法,接着介绍了配合后缀数组的最长公共前缀LCP(Longest Common Prefix)的计算方法,并给出一个线性时间内计算height数组(记录跨度为1的LCP值的数组)的算法。
为了让读者对如何运用后缀数组有一个感性认识,还介绍了两个应用后缀数组的例子:多模式串的模式匹配(给出每次匹配O(m+logn)时间复杂度的算法)以及求最长回文子串(给出O(nlogn)时间复杂度的算法)。
最后对后缀数组和后缀树作了一番比较。
【关键字】字符串,后缀,k-前缀比较关系,后缀数组,名次数组,后缀树,倍增算法,基数排序,最长公共前缀,RMQ问题,模式匹配,回文串,最长回文子串,【正文】在字符串处理当中,后缀树和后缀数组都是非常有力的工具,其中后缀树大家了解得比较多,关于后缀数组则很少见于国内的资料。
其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。
可以说,在信息学竞赛中后缀数组比后缀树要更为实用。
因此在本文中笔者想介绍一下后缀数组的基本概念、构造方法,以及配合后缀数组的最长公共前缀数组的构造方法,最后结合一些例子谈谈后缀数组的应用。
基本概念首先明确一些必要的定义:字符集一个字符集Σ是一个建立了全序关系的集合,也就是说,Σ中的任意两个不同的元素α和β都可以比较大小,要么α<β,要么β<α(也就是α>β)。
字符集Σ中的元素称为字符。
字符串一个字符串S是将n个字符顺次排列形成的数组,n称为S的长度,表示为len(S)。
S的第i个字符表示为S[i]。
子串字符串S的子串S[i..j],i≤j,表示S串中从i到j这一段,也就是顺次排列S[i],S[i+1],...,S[j]形成的字符串。
后缀数组入门(一)——后缀排序
后缀数组⼊门(⼀)——后缀排序前⾔后缀数组这个东西早就有所⽿闻,但由于很难,学了好⼏遍都没学会。
最近花了挺长⼀段时间去研究了⼀下,总算是勉强学会了⽤倍增法来实现后缀排序(据说还有⼀种更快的DC3法,但是要难得多)。
数组定义⾸先,为⽅便起见,我们⽤后缀i表⽰从下标i开始的后缀。
(相信⼤家都知道后缀是什么的)⾸先,我们需要定义⼏个数组:s:需要进⾏后缀排序的字符串。
SA i:记录排名为i的后缀的位置。
rk i:记录后缀i的排名。
pos i:同样记录排名为i的后缀的位置。
tot i:⽤于基数排序,统计i的排名。
要注意理解这些数组的定义,这样才能明⽩后⾯的内容。
第⼀次操作⾸先,让我们来⼀步步模拟⼀下第⼀次操作。
我们第⼀步是要将每个后缀按照第1个字符进⾏排序。
这应该还是⽐较简单的,不难发现可以初始化得到rk i=s i,pos i=i。
然后我们对其进⾏第⼀次排序。
注意,排序最好⽤O(n)的基数排序,⽤sort的话会多⼀个log。
具体的⼀些关于基数排序的细节可以见下。
关于基数排序后缀排序中的基数排序,其实相当于将⼆元组(rk i,pos i)进⾏排序。
⾸先,第⼀步⾃然是清空tot数组。
加1。
然后,从1到n枚举,将tot rki接下来是⼀遍累加,求出每⼀个元素的排名。
然后从n到1倒序枚举,更新SA数组即可。
接下来的操作接下来⾃然是要对每个后缀前2个字符进⾏排序了。
暴⼒的⽅法就是再重新排序⼀遍。
但实际上,在确定了第1个字符的⼤⼩关系后,我们就不需要如此⿇烦了。
因为后缀i的第2个字符,实际上就是后缀i+k的第1个字符。
因此我们通过第⼀次排序,就可以直接确定第2个字符的⼤⼩关系了。
于是我们就可以重新⽤pos数组将这个⼤⼩关系记录下来,再次排序。
然后就是按照这种⽅法来倍增处理第4个字符、第8个字符、第16个字符... ...重复此操作直⾄所有后缀各不相同即可。
这样的总复杂度就是O(nlogn)的了。
具体实现还是有很多细节的,实在没理解的可以根据代码再研究⼀下。
后缀数组及其应用
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)
关于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)时间内解决这一问题
后缀数组的两种主流构造方法
倍增算法(Double Algorithm) O(nlogn)
三分算法(Difference Cover modulo 3 ) O(N)
倍增算法(Double Algorithm)
总体来说,倍增算法的思想与ST的思想差不多。将后缀 长度依次分为1,2,4,8,。。。,2^k进行排序。进行当 前排序时利用到上次的排序结果。
S=“BANANAS”。
BANANAS 的后缀树
BANANAS 的Trie
后缀树在处理字符串问题上有着得天独厚的空间优势和速 度优势,在最坏情况下, 后缀树的节点数也不会超过2N。主 流的构造方法是由Esko Ukkonen 于1995年发明的一种线 性构造法,理论时间复杂度为O(N)。非常优秀。
国家集训队2004论文集 许智磊
证。
设 Suffix(SA[i])=u,Suffix(SA[j])=v,Suffix(SA[k])=w。
由 u=LCP(i,j)v 得 u=pv;同理 v=pw。 于是 Suffix(SA[i])=pSuffix(SA[k]),即 LCP(i,k)≥p。 (1)
不难看出,这种做法是很笨拙的,因为它没有利用到各个后缀之间的有机 联系,所以它的效率不可能很高。即使采用字符串排序中比较高效的 Multi-key Quick Sort,最坏情况的时间复杂度仍然是 O(n2)的,不能满足我们的需要。
第 2 页 共 11 页
IOI2004 国家集训队论文 许智磊
下面介绍倍增算法(Doubling Algorithm),它正是充分利用了各个后缀之间的 联系,将构造后缀数组的最坏时间复杂度成功降至 O(nlogn)。
后 缀 数 组 后 缀 数 组 SA 是 一 个 一 维 数 组 , 它 保 存 1..n 的 某 个 排 列 SA[1],SA[2],...SA[n],并且保证 Suffix(SA[i])<Suffix(SA[i+1]),1≤i<n。也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入 SA 中。
出 SA2k 的复杂度就是 O(nlogn)。更聪明的方法是采用基数排序,复杂度为 O(n)。 求出 SA2k 之后就可以在 O(n)的时间内根据 SA2k 构造出 Rank2k。因此,从 SAk
和 Rankk 推出 SA2k 和 Rank2k 可以在 O(n)时间内完成。 下面只有一个问题需要解决:如何构造出 SA1 和 Rank1。这个问题非常简单:
IOI2004 国家集训队论文 许智磊
后缀数组
安徽省芜湖市第一中学 许智磊
后缀数组 模板题
后缀数组模板题后缀数组是一种用于处理字符串的数组,其中每个元素都是原字符串的一个后缀。
在算法和数据结构中,后缀数组常用于解决字符串相关的问题,例如字符串匹配、最长重复子串等。
以下是一个使用后缀数组解决最长重复子串问题的模板题:题目描述:给定两个字符串s和p,找出s中最长也是p中的子串的长度。
如果不存在这样的子串,则返回0。
输入格式:输入的第一行包含一个字符串s,长度不超过100000。
输入的第二行包含一个字符串p,长度不超过100000。
输出格式:输出一个整数,表示s中最长也是p中的子串的长度。
示例:输入:abcdefgabcdfeg输出:4解释:在字符串s和p中,最长的公共子串是"efg",其长度为4。
算法思路:1. 构建后缀数组:首先将字符串s转换为后缀数组SA,可以使用KMP算法或Manacher算法。
2. 构建部分匹配数组PM:对于后缀数组SA中的每个元素,统计以该元素结尾的子串在p中出现的次数,保存在部分匹配数组PM中。
注意,PM的长度应为SA+1,因为我们需要包括空字符串。
3. 查找最长公共子串:遍历后缀数组SA中的每个元素,对于每个元素i,从PM[i]开始向前搜索,找到最长的公共子串的长度。
具体做法是,对于每个元素i,如果PM[i]不为0,则说明以i结尾的子串在p中出现过。
从PM[i]开始向前搜索,找到最长的公共子串的长度。
如果PM[i]为0,则说明以i结尾的子串在p中没有出现过,直接跳过该元素。
4. 返回结果:返回找到的最长公共子串的长度。
后缀数组 模版
后缀数组模版
后缀数组模版,提升字符串处理效率的利器。
在计算机科学领域,字符串处理是一个非常重要的课题。
而后缀数组作为一种
重要的数据结构,被广泛应用于字符串处理中,能够大大提升字符串处理的效率。
后缀数组是一个存储字符串所有后缀的数组,它能够快速地解决很多与字符串
相关的问题,比如最长公共子串、最长回文子串等。
通过构建后缀数组,我们可以在O(nlogn)的时间复杂度内解决这些问题,而且在实际应用中,后缀数组的效率往往比其他数据结构要高。
在实际应用中,后缀数组被广泛应用于字符串匹配、模式识别、基因序列分析
等领域。
比如在搜索引擎中,后缀数组可以用来加速字符串的匹配,提高搜索效率;在基因序列分析中,后缀数组可以用来寻找重复的基因序列,帮助科学家们更好地理解基因的结构和功能。
除此之外,后缀数组还可以被用来构建后缀树、解决最长公共前缀等问题,可
以说是一个非常强大的工具。
总的来说,后缀数组作为一种重要的数据结构,在字符串处理中起着举足轻重
的作用。
它能够大大提高字符串处理的效率,应用范围广泛,是计算机科学领域中的一把利器。
希望未来能够有更多的研究者投入到后缀数组的研究中,为其在实际应用中发挥更大的作用做出贡献。
后缀数组的定义及实现
后缀数组的定义及实现1 后缀数组的定义1.1 后缀数组的相关概念(1) 文本串的后缀:对于符号表Σ上的长度为n的文本串T[1...n],文字串T的后缀是指从第i个字符开始到T的末尾所形成的子串T[i...n],1 ≤i ≤n。
(2) 子串:在长度为n的文字串T中,下标从i到j的连续的(j-i+1)个字符所组成的字符序列就是文字串T的一个长度为( j – i + 1)子串,其中1 ≤i ≤j ≤n。
(3) 后缀数组:后缀数组SA是T的所有后缀T0, T1, ... T n-1的一个字典序排序,即SA[i] = j表示后缀T j是字符串集合{T0, T1, ... T n-1}中第i个最小字符串。
排序后的后缀数组具有如下性质:T SA[0] < T SA[1] < …< T SA[n - 1]下图为后缀数组及其名次示例(T = acaaccg$)此图表示描述了了SA中所需要得到的SA[i]和Rank[i],其中Rank[i] = j表示T中的第i个后缀在所有后缀的字典排名为j。
SA[i] = j的两种表示理解方式:(1) 表示T j在字符串集合中排名为i (2) 表示排名为i 的后缀的Position。
1.2 后缀数组的相关数组解释为了理解后缀数组,我们需要理解以下两个数组的含义以及它们之间的相互关系:SA数组(Pos数组):SA数组是我们需要直接使用的数组,SA数组是FM-index的基础,后面的bwt正是在SA的基础上进行变化得到文本L。
在1.1中提到:SA[i] = j表示后缀T j是字符串集合{T0, T1, ... T n-1}中第i个最小字符串。
因此SA[i] = j的两种表示理解方式:(1)表示T j在字符串集合中排名(字典序排名)为i。
(2)表示排名为i的后缀的Position。
Rank数组:Rank数组是后缀数组的逆数组SA-1,即若SA[j] = i,则Rank[i] = j,其实质反映了T中的第i个后缀在所有后缀中的字典序排名为j。
后缀数组(SA)
后缀数组(SA)SA 的⽤处(经典题型):求LCP,LCS,本质不同⼦串数出现 xxx 次⼦串,字符串中不重叠地出现⾄少两次的最长串长度最长AA式⼦串,连续的若⼲相同⼦串(AABB式)⾸先说⼀下,本篇博客是本⼈在oi-wiki学习后缀数组是写下的笔记,更详细的证明等见后缀数组主要包含两个数组:sa[ ]和rk[ ]。
sa[i]表⽰所有后缀中字典序第i⼩的那个后缀。
rk[i]表⽰i后缀的字典序排名。
求后缀数组模板提交处:其中倍增求SA的代码如下(使⽤基数排序):#include <iostream>#include <algorithm>#include <cmath>#include <cstdio>#include <cstring>#include <string>#define N 2000010template<typename T> inline void read(T &x) {x = 0; char c = getchar(); bool flag = false;while (isdigit(c)) {if (c == '-') flag = true; c = getchar(); }while (isdigit(c)) {x = (x << 1) + (x << 3) + (c ^ 48); c = getchar(); }if (flag) x = -x;}using namespace std;char s[N];int sa[N], rk[N], oldrk[N], id[N], p, n, w, limi;int bin[N];int main() {scanf("%s", s + 1);n = strlen(s + 1);for (register int i = 1; i <= n; ++i) ++bin[rk[i] = s[i]];for (register int i = 1; i <= 300; ++i) bin[i] += bin[i - 1];for (register int i = n; i > 0; --i) sa[bin[rk[i]]--] = i;limi = 300;//注意是limi不是p,因为for⼀开始时不会执⾏limi = pfor (w = 1; w < n; w <<= 1, limi = p) {//处理idp = 0; //注意p = 0for (register int i = n; i > n - w; --i) id[++p] = i;//注意"i"for (register int i = 1; i <= n; ++i)if (sa[i] > w) id[++p] = sa[i] - w;//注意"sa[i] - w" 注意"sa[i]"//处理samemset(bin, 0, sizeof(bin));for (register int i = 1; i <= n; ++i) ++bin[rk[id[i]]];for (register int i = 1; i <= limi; ++i) bin[i] += bin[i - 1];for (register int i = n; i > 0; --i) sa[bin[rk[id[i]]]--] = id[i];//处理rk(去重)memcpy(oldrk, rk, sizeof(rk));p = 0; //注意p = 0for (register int i = 1; i <= n; ++i)//注意"i - 1" 注意"oldrk"rk[sa[i]] = (oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) ? p : ++p; //注意 + wif (p == n) break;//⼩优化:如果已经排好序了,就不⽤再排了。
字符串--后缀数组的运用
字符串--后缀数组的运⽤后缀数组的运⽤主要体现在三个数组的使⽤之中1.sa[i]=n;表⽰的是排名是第i名的是第n个后缀2.rank[n]=i;表⽰的是第n个后缀在排序中是排在第⼏位的,它和sa数组是相反的。
3.height[ ]表⽰的是相邻的两个后缀之间的最长的公共前缀。
这三种数组的运⽤是有很多种,先总结两种。
⼀,求出现⾄少k次的最长不重复⼦串这就是height[ ]最典型的运⽤。
⼀般的思路是⼆分总的字符串的长度,来寻找符合条件的height[ ]中的段落,⼀旦出现符合条件的段落就记录下来,同时输出就可以。
例题题意:给出n个字符串,要求你找出⾄少在(n)/2个字符串中出现过的最长字串。
思路;这是⼀道典型的使⽤height[ ]数组的题⽬,这⾥只需要将给出的字符串都合并起来,同时在每⼀个合并的地⽅加上⼀个从来都没有出现过的字符,避免在两个字符串相交的地⽅出现符合条件的字串。
同时在最后合并的最后加上⼀个没有出现过的最⼩的字符。
这是为了,在建⽴三个数组的时候更好操作。
所以这题我们只要对⼀个长度进⾏⼆分,然后在height[ ]数组中分段就好了。
那么我们选择什么长度呢,这⾥只要选择所有给出的串中最长的⼀个,显然这是很符合条件的。
那么下⾯就是代码#include<cstdio>#include<cstring>#include<algorithm>#include<iostream>#include<string>#include<vector>#include<stack>#include<bitset>#include<cstdlib>#include<cmath>#include<set>#include<list>#include<deque>#include<map>#include<queue>using namespace std;typedef long long ll;const double PI = acos(-1.0);const double eps = 1e-6;const int mod =1000000;const int maxn =140110;char s[1100];int str[maxn];int Rank[maxn],height[maxn];int sa[maxn],t[maxn],t2[maxn],c[maxn];int belong[maxn],visit[200];void build_sa(int * s,int n,int m){int i,*x = t,*y = t2;for(i = 0;i < m;i++)c[i] = 0;for(i = 0;i < n;i++)c[ x[i] = s[i] ]++;for(i = 1;i < m;i++)c[i] += c[i-1];for(i = n-1;i >= 0;i--) sa[--c[x[i]]] = i;for(int k = 1;k <= n;k <<= 1){int p = 0;for(i = n - k;i < n;i++) y[p++] = i;for(i = 0;i < n;i++) if(sa[i] >= k) y[p++] = sa[i] - k;for(i = 0;i < m;i++) c[i] = 0;for(i = 0;i < n;i++) c[ x[y[i]] ]++;for(i = 0;i < m;i++) c[i] += c[i-1];for(i = n-1;i >= 0;i--) sa[--c[x[y[i]]]] = y[i];swap(x,y);p = 1; x[sa[0]] = 0;for(i = 1;i < n;i++)x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1] + k] == y[sa[i] + k] ? p-1:p++;if(p >= n) break;m = p;}}void calheight(int * s,int n){int i,j,k = 0;for(i = 0;i < n;i++)Rank[sa[i]] = i;for(i = 0;i < n;i++){if(k) k--;int j = sa[Rank[i]-1];while(s[i+k] == s[j+k]) k++;height[Rank[i]] = k;}}int Judge(int n,int len,int num){int i,j,k;int cnt=0;memset(visit,0,sizeof(visit));visit[0]=1;if(!visit[belong[sa[0]]]){cnt++;visit[belong[sa[0]]]=1;}for(i=1;i<n;i++){if(height[i]<len){cnt=0;memset(visit,0,sizeof(visit));visit[0]=1;if(!visit[belong[sa[i]]]){cnt++;visit[belong[sa[i]]]=1;}}else{if(!visit[belong[sa[i]]]){cnt++;visit[belong[sa[i]]]=1;}}if(cnt>=num) return 1;}return 0;}void out(int n,int len,int num){int i,j,cnt=0;memset(visit,0,sizeof(visit));visit[0]=1;if(!visit[belong[sa[0]]]){cnt++;}visit[belong[sa[0]]]=1;for(i=1;i<n;i++){if(height[i]<len){if(cnt>=num){for(j=sa[i-1];j<sa[i-1]+len;j++) printf("%c",str[j]-20);printf("\n");}cnt=0;memset(visit,0,sizeof(visit));visit[0]=1;if(!visit[belong[sa[i]]]){cnt++;visit[belong[sa[i]]]=1;}}else{if(!visit[belong[sa[i]]]){cnt++;visit[belong[sa[i]]]=1;}}}if(cnt>=num){for(j=sa[n-1];j<sa[n-1]+len;j++)printf("%c",str[j]-20);printf("\n");}}int main(){int n;int flag=1;while(scanf("%d",&n)==1&&n){if(!flag) printf("\n");else flag=0;int i,j,k;int pos=0,cnt=1,left=0,right=0;memset(belong,0,sizeof(belong));for(i=1;i<=n;i++){scanf("%s",s);int num=strlen(s);right=max(right,num);for(j=0;j<num;j++){str[pos+j]=int(s[j])+20;belong[pos+j]=i;}str[pos+num]=cnt++;pos=pos+num+1;}str[pos]=0;build_sa(str,pos+1,150);calheight(str,pos+1);int max_x=0;while(left<=right){int mid=(left+right)>>1;if(Judge(pos+1,mid,n/2+1)){max_x=mid;left=mid+1;}else{right=mid-1;}}//cout<<max_x<<"....."<<endl;if(max_x==0){printf("?\n");}else{out(pos+1,max_x,n/2+1);}}return 0;}2.求出现k次或者是⾄少k次的最长可重叠⼦串这个问题和上⾯的问题就很类似,实际上差别就不会很⼤,上⾯的要求是不能重叠,⽽这⾥则是可以重叠的,那么反⽽这个类型是更加简单的。
字符串匹配(三)----后缀数组算法
字符串匹配(三)----后缀数组算法⼀、什么是后缀数组: 字符串后缀Suffix指的是从字符串的某个位置开始到其末尾的字符串⼦串。
后缀数组 Suffix Array(sa)指的是将某个字符串的所有后缀按字典序排序之后得到的数组,不过数组中不直接保存所有的后缀⼦串,只要记录后缀的起始下标就好了。
⽐如下⾯在下⾯这张图中,sa[8] = 7,表⽰在字典序中排第9的是起始下标为7的后缀⼦串,这⾥还有⼀个⽐较重要的数组rank,rank[i] : sa[i]在所有后缀中的排名,⽐如rk[5]=0,表⽰后缀下标为5的⼦串在后缀数组中排第0个; rank数组与sa数组互为逆运算,rk[sa[i]]=i; 现在假如我们已经求出来了后缀数组,然后直接对已经排好序的后缀数组进⾏⼆分查找,这样就能匹配成功了,下⾯贴出代码:import java.util.Arrays;public class SuffixArrayTest {public static void main(String[] args) {match(); // 得到结果是5}static void match(){String s = "ABABABABB";String p = "BABB";Suff[] sa = getSa(s); // 后缀数组int l = 0;int r = s.length()-1;// ⼆分查找,nlog(m)while(r>=l){int mid = l + ((r-l)>>1);// 居中的后缀Suff midSuff = sa[mid];String suffStr = midSuff.str;int compareRes;// 将后缀和模式串⽐较,O(n);if (suffStr.length()>=p.length()) {compareRes = suffStr.substring(0, p.length()).compareTo(p);}else {compareRes = pareTo(p);}// 相等了输出后缀的起始位置if(compareRes == 0){System.out.println(midSuff.index);break;}else if (compareRes<0) {l = mid + 1;}else {r = mid - 1;}}}/*** 直接对所有后缀排序,因为字符串的⽐较消耗O(N),所以整体为N²log(N)* @param src* @return*/public static Suff[] getSa(String src){int strLength = src.length();// sa 即SuffixArray,后缀数组// sa 是排名到下标的映射,即sa[i]=k说明排名为i的后缀是从k开始的Suff[] suffixArray = new Suff[strLength];for (int i = 0; i < strLength; i++) {String suffI = src.substring(i); //截取后缀suffixArray[i] = new Suff(suffI, i);}Arrays.sort(suffixArray); //依据Suff的⽐较规则进⾏排序return suffixArray;}static class Suff implements Comparable<Suff>{String str; //后缀内容int index; //后缀的起始下标public Suff(String str, int index) {super();this.str = str;this.index = index;}@Overridepublic int compareTo(Suff o2) {return pareTo(o2.str);}@Overridepublic String toString() {return "Suff{"+"str='"+str+"\'"+",index="+index+"}";}}}⼆、倍增法 上⾯求后缀数组的⽅式时间复杂度为n²log(n),⼀般来说,时间复杂度只要达到了n平⽅级别都要想办法降低,于是就有⼀种叫做倍增法的⽅法来求后缀数组,基本思想就是: 1、先将每个字符排序得到sa,rank数组, 2、然后给每个字符增添⼀个字符,这样就变成了两个字符,最后⼀个字符⽆法增添字符,就需要处理好边界问题。
后缀数组——处理字符串的有力工具
后缀数组 罗穗骞
例 10:长度不小于 k 的公共子串的个数(pku3415) ……………23 2.4 多个字符串的相关问题 …………………………………………………23
例 11:不小 于 k 个字符串中的最长子串(pku3294) ……………………24 例 12:每个字符串至少出现两次且不重叠的最长子串(spoj220)……24 例 13:出现或反转后出现在每个字符串中的最长子串(pku3294)……24 三、结束语 …………………………………………………………………………25 3.1 总结 ………………………………………………………………………25 3.2 参考文献 …………………………………………………………………25 3.3 致谢 ………………………………………………………………………25
1.2倍增算法
对每个字符开始的长度为 2k 的子字符串进行排序,求出排名,即 rank 值 。k 从 0 开始,每次加 1,当 2k 大于 n 以后,每个字符开始的长度为 2k 的子字符串 便相当于所有的后缀。并且这些子字符串都一定已经比较出大小,即 rank 值中 没有相同的值,那么此时的 rank 值就是最后的结果。每一次排序都利用上次长 度为 2k-1 的字符串的 rank 值,那么长度为 2k 的字符串就可以用两个长度为 2k-1 的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为 2k 的字 符串的 rank 值。以字符串“aabaaaab”为例,整个过程如图 2 所示。其中 x、y 是表示长度为 2k 的字符串的两个关键字。
例 1:最长公共前缀 ……………………………………………………17 2.2 单个字符串的相关问题 …………………………………………………17
2.2.1 重复子串 ………………………………………………………17 例 2:可重叠最长重复子串 ………………………………………17 例 3:不可重叠最长重复子串(pku1743)…………………………18 例 4:可重叠的最长重复子串(pku3261)…………………………19
后缀数组
1
aa 11 1 aab0 aa 1 13 3 b0 3
1
ab 12 2 ab00 ab 2 20 5 00 0
2
b0 20 3 b000 b0 3 30 7 00 0
至此,后缀数组就已经求出来了 SA[]={4,6,8,1,2,3,5,7}; 想一想: 为什么当所有名次都不相同时就不用再往下比了 呢? 接下来就是程序实现了
for(i=0;i<n;i++) wv[i]=x[y[i]];
/*以下几行,在第二关键字排好序的基础上,对第一关键字排序*/ for(i=0;i<m;i++) ws[i]=0; for(i=0;i<n;i++) ws[wv[i]]++; for(i=1;i<m;i++) ws[i]+=ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];
代码!!!
void CalHeight(int *SA,int *height,int n) { int i,j,k=0; for(i=1;i<=n;i++) rank[sa[i]=i; for(i=0;i<n;i++) { if(k!=0) k=k-1; else k=0; j=sa[rank[i]-1]; //suffix(j)排在suffix(i)前一名 while(r[i+k]==r[j+k]) k++; height[rank[i]]=k; } }
• 例 1 :最长公共前缀
给定一个字符串,询问某两个后缀的最长公共前缀。 // 直接套用, ans=min( height[ i ] )+rmq k<i<=j
详细解析后缀数组(RMQ及LCP)
经典的RMQ (Range Minimum Query)问题!!!
➢线段树、排序树 —— O(nlogn)预处理 , O(logn)每次询问 ➢标准RMQ方法 —— O(n)预处理 , O(1)每次询问
后缀数组——辅助工具
采用一种“神奇的”方法,可以在O(n)时间内计算出height数组 采用标准RMQ方法在O(n)时间内进行预处理 之后就可以在常数时间内算出任何的LCP(i,j)
名次数组
后缀数组——构造方法
如何构造后缀数组?
把n个后缀当作n个字符串,按照普通的方法进行排序 —— O(n2)
低效的原因 —— 把后缀仅仅当作普通的、独立 的字符串,忽略了后缀之间存在的有机联系。
后缀数组——构造方法
u[1..k] ,len(u)≥k 对字符串u,定义uk =
u
,len(u)<k
LCP Theorem 对任何1≤i<j≤n LCP(i,j)=min{LCP(k-1,k) | i+1≤k≤j} 称j-i为LCP(i,j)的“跨度”,LCP Theorem意义为: 跨度大于1的LCP值可以表示成一段跨度等于1的LCP值的最小值
后缀数组——辅助工具
LCP(i,j)=3
4=LCP(i,i+1)
后缀是一种特殊的子串 从某个位置i开始到整个串的末尾结束 S的从i开头的后缀等价于S[i..len(S)]
后缀数组——定义和符号
约定一个字符集Σ 待处理的字符串约定为S,约定len(S)=n 规定S以字符“$”结尾,即S[n]=“$”
“$”小于Σ中所有的字符 除了S[n]=“$”之外,S的其他字符都属于Σ
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
其中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[],高 效的比较;充分利用了后缀之间的联系;
缺点: 该算法实现过于复杂,细节较多,且代码量较长,对于 一名信息竞赛的选手而言,在短短的5个小时内,要完全正 确地写出后缀树谈何容易。在各种各样的比赛中,我们要 选择实现相对简单,且效率较高的算法和数据结构。
随着广大OIERS的呼声,
后缀数组横空出世!!
*后缀数组 (Suffix Arrary)
后缀树的特点: 后缀树实质上是一棵字典树。从根节点到任意叶子节点 都对应了原串的一个后缀。但与Trie结构不同的是树上的每一 条边记录的不是单个字母,而是一个字符串。这样一来,将 空间从Trie的O(N^2)降到O(N) 。 同时,Trie的构造时间是O(N^2),而后缀树的构造时间仅 为O(N)。这也是后缀树广为人知的原因。 下面我们看个实例:
后缀数组的构造方法:
比较直观的想法——暴力排序: 把n个后缀预处理出来,快排,堆排,归并等,时 间复杂度O(nlogn),看上去还不错。 但仔细想想,两个字符串间比较有个strcmp(),这 明显不是线性。况且kmp也需要O(N+M)的时间,这样 一来时间复杂度接近O(n^2)。 当N>10000时就悲剧了。。。。。
通过上面的例子,通过k-rank[]可以在O(1)的时间内 完成suffix(i)和suffix(j)的比较。这样就充分利用了后缀之 间有机的联系。 具体实现当然不是枚举每两个串进行比较。进一步想 想,这样比较不就是将每个后缀看作一个元素,k-rank[i]作 为第一关键字,k-rank[i+k]作为第二关键字进行排序吗? 这时,排序方法又是一个大问题。虽然快排等比较型排序 可以做到O(nlogn),但是还不能满足后缀树组的高效要求。 每个元素只有两个关键字,而且关键字分布比较集中,相信聪 明的你已经想到了:
后缀数组及 其应用
本文探讨内容:
1、后缀树组的概念及构造方 法; 2、后缀树组的相关应用;
有关后缀树(Suffix Tree ):
提到后缀数组,我们不由自主地会想到后缀树。后缀树 (Suffix tree)是一种数据结构,能快速解决很多关于字符 串的问题。后缀树的概念最早由Weiner 于1973年提出,既 而由McCreight 在1976年和Ukkonen在1992年和1995年加 以改进完善。
有了这个性质,我们就可以根据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次,算法是线性的, 算法总流程
S=“BANANAS”。
BANANAS 的后缀树
BANANAS 的Trie
后缀树在处理字符串问题上有着得天独厚的空间优势和速 度优势,在最坏情况下, 后缀树的节点数也不会超过2N。主 流的构造方法是由Esko Ukkonen 于1995年发明的一种线 性构造法,理论时间复杂度为O(N)。非常优秀。
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)
在此,我向大家介绍后缀数组的强力外援
——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)之间的最 小值。
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]
接着将筒中的数依次倒出,得到: 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)。
O(N)
Height[]数组及其高效计算
还是先看定义:
height[i] = LCP(sa[i-1],sa[i])
即排名相邻的两个后缀的最长公共前缀长度。 对于height[]数组的计算,我们并不能顺序递推。需要充 分利用字符串之间的联系,改变他的计算顺序。 所以,我们定义h[i]:
h[i] = height[rank[i]]
现在只需求出1-rank[]和1-sa[]就可以每次通过O(n)的排 序转移。至于求1-rank[]与1-sa[],把原串中的字符排序即可, 快排或基数排序都可以,均不影响算法的时间复杂度。但是建 议使用基数排序。 O(n)排序 原始串S 1-rank[],1-sa[] O(n)排序 L-rank[],l-sa[].
2、将k每次加倍,时间压缩成logn级。
3、类似于动规中的ST。
讲了这么多,我们来看个实例:
最长公共子串问题。
最长公共子串问题: 给定两个字符串S1[],S2[]。求出他们的最长公共子串。 例如: s1=“she handsome”,s2=“he slim”。则输出字符 串”he”。(N<=10^5) 首先明确一个事实,任何子串都是某一后缀的前缀。那么, 很直观的想法,把两个串接起来,中间用‘$’隔开。对这个新 的字符串求后缀树组,每次求出排名相邻的两个后缀的最长前 缀即可。当然,这个最长前缀不能跨过‘$’。 新问题出现了: 最长公共前缀怎么求?难道一个一个比较,那么时间复杂 度又回到了O(N^2)级了。看来,如果只有sa[],后缀数组还 不够强大。
首先给出一些新的定义: 1、字符串均以“000000….”结尾,‘0’为最 小字符; 2、定义k-rank[],即在比较每个后缀前k个字符 的情况下,suffix(i)的名次记做k-rank[i].类似地定义 k-sa[]。
当前求2k-rank[],2k-sa[]。
K个字符 K个字符 K个字符 K个字符
关于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)时间内解决这一问题
现在,我们可以很好的解决刚才的最长公共子串问题了, 构造好后缀数组后,答案就是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。