算法合集之《后缀数组——处理字符串的有力工具》
【字符串】后缀数组

【字符串】后缀数组后缀排序倍增算法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:本⽂中的^表⽰平⽅⽽不是异或倍增⾸先读⼊字符串之后我们现根据单个字符排序,当然也可以理解为先按照每个后缀的第⼀个字符排序。
对于每个字符,我们按照字典序给⼀个排名(当然可以并列),这⾥称作关键字。
接下来我们再把相邻的两个关键字合并到⼀起,就相当于根据每⼀个后缀的前两个字符进⾏排序。
想想看,这样就是以第⼀个字符(也就是⾃⼰本⾝)的排名为第⼀关键字,以第⼆个字符的排名为第⼆关键字,把组成的新数排完序之后再次标号。
没有第⼆关键字的补零。
(全国百强校)广东华南师范大学附属中学高中信息技术资料:处理字符串的有力工具-后缀数组

f(n) = O(n) + f(2n/3) f(n) ≤ c×n + f(2n/3)
f(n)≤c×n+c×(2n/3)+c×(4n/9)+……≤3c×n 所以 f(n)=O(n)
由此看出,DC3算法是一个优秀的线性算法!
后缀数组的应用
例1:如果字符串L同时出现在字符串A
和字符串B中,则称字符串L是字符串A 和字符串B的公共子串。 给定两个字符串A和B,求最长公共子串。 例如:字符串“aaaba”和“abaa”的 最长公共子串为“aba”
DC3算法 复杂 难以实现
仅40行代码
DC3算法
(1)、先将后缀分成两部分,然后对 第一部分的后缀排序。
(2)、利用(1)的结果,对第二部分 的后缀排序。
(3)、将(1)和(2)的结果合并, 即完成对所有后缀排序。
(1)、将后缀分成两部分
字符的编号从0开始。 将后缀分成两部分: 第一部分是后缀k(k模3不等于0) 第二部分是后缀k(k模3等于0)
例1 最长公共子串
字符串的任何一个子串都是这个字符 串的某个后缀的前缀。求A和B的最长公 共子串等价于求A的后缀和B的后缀的最 长公共前缀的最大值。
将第二个字符串写在第一个字符串后 面,中间用一个没有出现过的字符隔开, 再求这个新的字符串的后缀数组。
如何高效的计算height数组?
如果按height[2],height[3],……, height[n]的顺序计算,最坏情况下时间复 杂度为O(n2)。这样做并没有利用字符串的 性质。定义h[i]=height[rank[i]],也就是 suffix(i)和在它前一名的后缀的最长公共前 缀。h数组有以下性质:
一种外存后缀数组结构算法工程实现及特性评估.doc

一种外存后缀数组结构算法工程实现及特性评估第一章绪论1.1研究背景与意义随着对后缀数组研究的不断深入,后缀数组在处理文本数据时所具备的优势日益凸显,被广泛的应用于生物信息学、络检索、频繁字符串挖掘以及顺序分析和聚类分析等领域。
在生物信息学中,DNA 序列可以看作是一个在{A、C、G、T}字符集上的文本数据。
这样,对DNA 的处理也就相当于对一个文本数据进行处理。
例如,我们要判断一个小的DNA 片段属于哪一个完整的DNA 序列时,我们需要将小的DNA 片段与每一个完整的DNA 序列进行配对,如果将DNA 如上面所说看成是一个由A、C、G、T 四个字母组成的文本数据,那么这个DNA匹配问题就可以转化为如何在一个文本数据集DNA 库)中找出某个特定字符串DNA 片段)所在位置的问题。
在络检索中,搜索引擎的主要工作就是从海量的络数据资源中快速、准确地检索出用户所查找的信息,而这实际上也是在一个文本数据集络资源)中找出某个特定字符串用户输入的关键字)的问题。
为了解决这类问题,后缀树和后缀数组作为两种文本数据索引结构被提了出来。
后缀树与后缀数组是文本数据处理中两个强有力的工具。
后缀树的概念最早由Weiner[2]于1973 年提出,在处理文本数据时具有适应性强、以其为基础设计的算法易于理解等特点。
但是后缀树的构造和存储需要消耗大量的内存空间,严重的限制了后缀树的应用范围。
1993 年,Manber 和Myers[1]提出了一种用来替代后缀树的数据结构,即后缀数组。
他们将一个字符串的后缀数组定义为该字符串所有后缀按字典序排列的次序。
1.2后缀数组构造算法的研究现状在后缀数组被提出以前,后缀树在文本数据处理的相关领域被广泛应用。
但后缀树的建立与存储需要占用大量的空间,并且各领域需要处理的文本数据日益增长,后缀树空间效率低的劣势越发凸显,导致了后缀树构造算法“内存瓶颈”[5]问题的产生,极大的限制了后缀树的应用范围。
后缀树与后缀数组

• 显然, 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
后缀数组

OI笔记]后缀数组学习笔记--后缀数组解题方法总结2010-04-15 07:37后缀数组是处理字符串的有力工具。
后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也并不逊色,而且它比后缀树所占用的内存空间小很多。
可以说,后缀数组比后缀树要更为实用。
自从拜读了罗穗骞大牛的WC2009论文《后缀数组——处理字符串的有力工具》后,经过若干星期的努力(中间有因某些原因而缓下来),终于把论文上面的练习题全部完成了,现在写写自己对后缀数组的理解和感悟。
在看本笔记时,请不要忘记了,这是笔记,而教材是《后缀数组——处理字符串的有力工具》。
一:后缀数组的实现1、定义:Suffix Array数组(SA数组)用于保存从小到大排好序之后的后缀。
RANK名次数组用来保存后缀S[i..n]在所有后缀中是第几小的后缀。
简单来说,SA数组表示的是“排第几的是谁”,RANK数组表示的是“你的排名是多少”。
2、求SA数组以及RANK数组的方法:详细的请转到罗穗骞大牛的论文,我的学习笔记重点不是要介绍这个。
3、对DA(倍增算法)的一些个人理解:由于我只学习了倍增算法,所以我只能谈谈我对它的理解。
DC3算法我没有去研究....DA算法我是根据罗穗骞的模板写的,根据自己的理解做了些许的小优化。
我们现在来看看罗穗骞大牛的模板:int wa[maxn],wb[maxn],wv[maxn],ws[maxn];int cmp(int *r,int a,int b,int l){return r[a]==r[b]&&r[a+l]==r[b+l];}void da(int *r,int *sa,int n,int m){int i,j,p,*x=wa,*y=wb,*t;for(i=0;i<m;i++) ws[i]=0;for(i=0;i<n;i++) ws[x[i]=r[i]]++;for(i=1;i<m;i++) ws[i]+=ws[i-1];for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;for(j=1,p=1;p<n;j*=2,m=p){for(p=0,i=n-j;i<n;i++) y[p++]=i;for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;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];for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;}return;}其实,我个人认为,对于这个算法以及代码,无需过分深入地理解,只需记忆即可,理解只是为了帮助记忆罢了。
国家集训队2009论文集后缀数组——处理字符

后缀数组 罗穗骞
例 10:长度不小于 k 的公共子串的个数(pku3415) ……………23 2.4 多个字符串的相关问题 …………………………………………………23
例 11:不小于 k 个字符串中的最长子串(pku3294) ……………………24 例 12:每个字符串至少出现两次且不重叠的最长子串(spoj220)……24 例 13:出现或反转后出现在每个字符串中的最长子串(pku3294)……24 三、结束语 …………………………………………………………………………25 参考文献 ……………………………………………………………………………25 致谢 …………………………………………………………………………………25
目录
后缀数组 罗穗骞
摘要 …………………………………………………………………………………4 关键字 ………………………………………………………………………………4 正文 …………………………………………………………………………………4 一、后缀数组的实现 …………………………………………………………………4
符 串 r 的 从 第 i 个 字 符 开 始 的 后 缀 表 示 为 Suffix(i) , 也 就 是 Suffix(i)=r[i..len(r)]。
大小比较:关于字符串的大小比较,是指通常所说的“字典顺序”比较,也 就是对于两个字符串 u、v,令 i 从 1 开始顺次比较 u[i]和 v[i],如果 u[i]=v[i]则令 i 加 1,否则若 u[i]<v[i]则认为 u<v,u[i]>v[i]则认为 u>v (也就是 v<u),比较结束。如果 i>len(u)或者 i>len(v)仍比较不出结果,那 么 若 len(u)<len(v) 则 认 为 u<v , 若 len(u)=len(v) 则 认 为 u=v , 若 len(u)>len(v)则 u>v。
数据结构编译原理 前缀和后缀

数据结构编译原理前缀和后缀前缀和和后缀分别是一种常用的数据结构和算法,它们在编译原理和其他一些计算机科学领域中都有广泛的应用。
前缀和是一种用来统计数组前缀和区间和的算法,也可以叫做累加和。
它的实现方式是先将原数组中的值依次相加得到一个新数组,然后在新数组中查询任意区间的和时,只需要进行一次减法操作即可得到区间和。
例如,给定一个数组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数组进行二分查找来快速匹配包含某个字符串模式的后缀。
窗外一叶?后缀树与后缀数组

窗外一叶?后缀树与后缀数组本文系转载:/post/hj7cv6m后缀树和后缀数组简直就是ACM 选手必备的知识啊,我已经在两次比赛中碰到过相关的问题了。
我甚至还写过一篇应用的文章,可是我真是井底之蛙啊,那时我还不知道这个叫后缀数组,还有更好的构造算法,还有很多的应用。
最近终于好好在这方面扫了个盲,在此小小地总结一下。
假设有一个长度为 n 的字符串T[0 … n);S(i) 表示 T 的从下标 i 开始的后缀,即T[i … n)。
那么 T 的后缀数组就是把 S(i) ~ S(n – 1) 这n 个后缀按字典序排好序的一个数组。
它对于查找T 的子串之类的问题是很有用的。
问题就在于怎样快速地把这些后缀排好序。
最简单的方法就是把所有S(i) 快速排序。
快速排序本身的时间是O(n log n),但是由于排序的对象是一个个字符串,所以每次比较的时间在最差情况下都会变成线性的(也就是 O(n) 的),因此总的时间在最差情况下可能会升到O(n2) 左右,这就很慢了。
对此,我学到了三个更快的算法。
1. Ukkonen 算法Ukkonen 算法先用 O(n) 的时间构造一棵后缀树,然后再用 O(n) 的时间从后缀树得到后缀数组。
在这个网址,介绍了作者Esko Ukkonen,并列出了他的一些论文;其中的一篇《On-line construction of suffix-trees》是可以下载的,里面就讲解了什么是后缀树,怎样在 O(n) 的时间内构造它,以及怎样从它得到后缀数组。
不过我一开始还没发现这篇论文,我是从Dan Gusfield 的《Algorithms on Strings, Trees and Sequences –COMPUTER SCIENCE AND COMPUTATIONAL BIOLOGY》这本书里学到这个算法的。
这本书在中国没的卖,想买的话,可以找代购网站去Amazon 买。
我是在 eMule 上搜到并下载的。
后缀数组——处理字符串的有力工具

后缀数组 罗穗骞
信息学奥林匹克
China Nation Olympiad in Informatics
国家集训队论文
题 目: 后缀数组——处理字符串的有力工具
作 者:
罗穗骞
指导教师:
张学东
学 校:
华南师范大学附属中学
完成时间:
2009年1月
IOI2009 国家集训队论文
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 的字符串的两个关键字 。
6
IOI2009 国家集训队论文
本页已使用福昕阅读器进行编辑。 福昕软件(C)2005-2009,版权所有, 仅供试用后。缀数组 罗穗骞
具体实现: int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; int cmp(int *r,int a,int b,int l) {return r[a]==r[b]&&r[a+l]==r[b+l];} void da(int *r,int *sa,int n,int m) { int i,j,p,*x=wa,*y=wb,*t; for(i=0;i<m;i++) ws[i]=0; for(i=0;i<n;i++) ws[x[i]=r[i]]++; for(i=1;i<m;i++) ws[i]+=ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i; for(j=1,p=1;p<n;j*=2,m=p) { for(p=0,i=n-j;i<n;i++) y[p++]=i; for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j; 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]; for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } return; }
后缀数组及其应用

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)。非常优秀。
字符串匹配(三)----后缀数组算法

字符串匹配(三)----后缀数组算法⼀、什么是后缀数组: 字符串后缀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、然后给每个字符增添⼀个字符,这样就变成了两个字符,最后⼀个字符⽆法增添字符,就需要处理好边界问题。
后缀数组代码详解

后缀数组代码详解说起来学了很久的后缀数组了思想还是很容易明⽩的最⼤的问题就是代码看不懂然后在不断模拟研究的过程中终于弄清楚了⼀点就决定写下来了不然⼜会忘的QAQ以下是代码ps:⾸先要弄懂基数排序定义:c[ ]数组:基数排序的桶x[ ]数组:类似于rank数组,保存当前排序到的以每个位置开始的⼀段字符串的排名sa[ ]数组:sa[i]保存第i个排名的字符串开头所在位置y[ ]数组:第⼆关键字排序,y[i]保存第i个排名的字符串开头所在位置1void BuildSa(int m) {2int *x = t, *y = t2; //这是对单个字符的排序3for (int i = 0; i < m; i++) c[i] = 0; // 清空c数组4for (int i = 0; i < n; i++) c[x[i] = s[i]]++; // 初始长度为1时, x[i] == s[i]5for (int i = 1; i < m; i++) c[i] += c[i - 1]; // 得到每个所在位置6for (int i = n - 1; i >= 0; i--) sa[--c[x[i]]] = i; // 排名为c[x[i]]的字符,是从i位置开始的,每记录⼀次排名就-1,到前⼀个位置,保证排名和对应位置不重复7for (int k = 1; k <= n; k <<= 1) {8 int p = 0; // 计数⽤的9 for (int i = n - k; i < n; i++) y[p++] = i; // 0是最⼩的,先把后⾯需要补0的放在最前⾯10 for (int i = 0; i < n; i++) if (sa[i] >= k) y[p++] = sa[i] - k; // sa[i] < k时不是第⼆关键字, sa[i]的排名是依次递增的,所以直接读⼊y数组,减去前⾯不是第⼆关键字的部分11 for (int i = 0; i < m; i++) c[i] = 0; // 清空c数组12 for (int i = 0; i < n; i++) c[x[y[i]]]++; // 此时在y中已经按第⼆关键字排好序,x中存的是第⼀关键字,再直接基数排序第⼀关键字即可13for (int i = 1; i < m; i++) c[i] += c[i - 1];14for (int i = n - 1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];15 swap(x, y);16// 离散化排名17 p = 1; x[sa[0]] = 0;18for (int i = 1; i < n; i++)19 x[sa[i]] = y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k] ? p - 1 : p++; // 两个关键字都相同则排名相同20if (p >= n) break; // p代表了不同排名数量,若数量已经达到n,则排序完毕,可直接退出21 m = p;22 }23 }这是得到sa数组的做法然⽽⼀个sa数组并没有什么⽤下⾯是得到height数组的算法具体证明可以参考这个论⽂写的很好啦就是没有注释...⾸先还是上代码定义:rank[ ]数组:与sa[ ]数组正好相反,rank[i] = j代表以i开始的后缀的排名 1void GetHeight() {2int k = 0;3for (int i = 0; i < n; i++) rank[sa[i]] = i;4for (int i = 0; i < n; i++) {5if (k) k--; // 因为h[i] >= h[i - 1] - 1(h[i]看论⽂的定义),所以k需要-16int j = sa[rank[i] - 1]; //得到排名前⼀位后缀开始位置7while (s[i + k] == s[j + k]) k++; //从后⾯k个开始⽐较,直到不相同为⽌8 height[rank[i]] = k;9 }10 }。
后缀树简介

后缀树一、字符串匹配1、字符串匹配问题的形式定义●文本(Text)是一个长度为n的数组T[1...n];●模式(Pattern)是一个长度为m且m≤n的数组P[1…m];●T和P中的元素都属于有限的字母表(alphabet);●如果0≤s≤n-m,并且T[s+1…S+m]=P[1…m],即对1≤j≤m,有T[s+j]=P[j],则说模式P在文本T中出现且位移为s,且称s是一个有效位移(validshift)。
如上图中,目标是找出所有在文本T=abcabaabcabac中模式P=abaa的所有出现。
该模式在此文中仅出现一次,即在位移s=3处,位移s=3是一个有效位移。
2、解决字符串匹配问题的常见算法●朴素的字符串匹配算法(NativeStringMatchingAlgorithm)●Knuth-Morris-Pratt字符串匹配算法(即KMP算法)●Boyer-Moore字符串匹配算法字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。
所以算法的总运行时间为预处理和匹配的时间的总和。
下面描述了常见字符串匹配算法的预处理和匹配时间。
上述字符串匹配算法均是通过对模式(Pattern)字符串进行预处理的方式来加快搜索速度。
对Pattern进行预处理的最优复杂度为O(m),其中m为Pattern字符串的长度。
而后缀树(SuffixTree)是一种对Text进行预处理的字符串匹配算法。
二、字典树(Trie)1、字典树定义字典树(Trie):是一种很特别的树状信息检索数据结构,如同其名,它的构成就像一本字典,可以让你快速的进行字符插入,字符搜索等。
字典树的核心思想是空间换时间,所以数据结构本身比较消耗空间。
但它利用了字符串的共同前缀(CommonPrefix)作为存储依据,以此来节省存储空间,并加速搜索时间。
Trie的字符串搜索时间复杂度为O(m),m 为最长字符串的长度,其查询性能与集合中的字符串的数量无关。
后缀数组——处理字符串的有力工具

后缀数组 罗穗骞
例 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
后缀树——精选推荐

后缀树后缀树内容提要本章主要介绍了后缀树的来源以及后缀树的应⽤背景,给出了后缀树的定义、性质、特征以及构造⽅法等理论基础,通过最长回⽂的查找、⼦串的查找等实例进⼀步说明了后缀树的特征及⽤途。
引⾔在计算机科学中,后缀树(也叫做PA T树,早期的形式是位置树)是⼀种数据结构,在某种程度上,它可以显⽰出⼀个给定字符串的后缀,且对于很多的字符串操作它能够⾮常快的实现。
字符串S的后缀树是这样⼀棵树,它的所有边都是⽤字符串来标⽰的,这样字符串S 的每⼀后缀都恰好的对应⼀条从根到叶⼦节点的路径。
这是以字符串S为后缀的基数树,更具体地说,这是⼀颗帕特⾥夏树。
为字符串S构造⼀颗这样的树耗费的时间和空间与字符串的长度呈线性关系。
这样的树⼀旦构造完成,⼏个操作能够被很快的执⾏,例如,在字符串S中定位⼀个字串,在允许⼀定数量的错误前提下定位⼀个字串,为⼀个标准表达式模式定位匹配的问题等等。
后缀树也为最⼤公共字串问题提供了⼀个第⼀线性时间的解决⽅案。
这种速度的提升带来了⼀定的开销:存储⼀个字符串的后缀树⽐存储字符串本⾝需要更⼤的空间。
历史在1973年,后缀树的概念是以位置树的形式被weiner⾸先提出来,随后Donald Knuth 称它为1973年的年度算法。
分别在1976年和1995年,McCreight和Ukkonen对它的结构进⾏了很⼤程度的简化。
Ukkonen提供了后缀树的第⼀个⽹络建设,即现在熟知的Ukkonen 算法,它是运⾏时间是最快的算法。
对于恒定⼤⼩的字母表来说,这些算法的运⾏时间都是线性的,并且⼀般情况下,它们的最坏的运⾏时间是O(n long n)。
在1997年Farach给出了第⼀个后缀树构造算法,对于所有的字母表,它都是最佳的。
特别的,对来⾃于⼀个多项式范围内的⼀个整数的字母表的字符串,这是第⼀个线性时间算法。
Farach算法成为了构造后缀树和后缀树组的新算法的基础,例如,在外部存储器中,它是压缩的和简洁的。
后缀树和后缀数组

后缀树和后缀数组基本概念子串:字符串S的子串S[i..j],i?j,表示S串中从i到j这一段,也就是顺次排列S[i],S[i+1],...,S[j]形成的字符串。
字符集:一个字符集Σ是一个建立了全序关系的集合,也就是说,Σ中的任意两个不同的元素α和β都可以比较大小,要么α<β,要么β<α(也就是α>β)。
字符集Σ中的元素称为字符。
字符串:一个字符串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]形成的字符串。
后缀:后缀是指从某个位置i开始到整个串末尾结束的一个特殊子串。
字符串S 的从i开头的后缀表示为Suffix(i),也就是Suffix(i)=S[i..len(S)] 例如S = mississippi,那么它的所有后缀为:Suffix(1) = mississippi = SSuffix(2) = ississippiSuffix(3) = ssissippiSuffix(4) = sissippiSuffix(5) = issippiSuffix(6) = ssippiSuffix(7) = sippiSuffix(8) = ippiSuffix(9) = ppiSuffix(10) = piSuffix(11) = iSuffix(12) = (empty)不难发现,S的任意一个子串一定是某一个后缀的前缀。
字符串的大小比较:指通常所说的“字典顺序”比较,也就是对于两个字符串u、v,令i从1开始顺次比较u[i]和v[i],如果u[i]=v[i]则令i加1,否则若u[i]<v[i]则认为u<v,u[i]>v[i]则认为u>v(也就是v<u),比较结束。
如果i>len(u)或者i>len(v)仍比较出结果,那么若len(u)<len(v)则认为u<v,若len(u)=len(v)则认为u=v,若len(u)>len(v)则u>v。
详细解析后缀数组(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)。
2.2.1 重复子串 ………………………………………………………18 例 2:可重叠最长重复子串 ………………………………………18 例 3:不可重叠最长重复子串(pku1743)…………………………18 例 4:可重叠的最长重复子串(pku3261)…………………………19
函数的第一步,要对长度为 1 的字符串进行排序。一般来说,在字符串的题 目中,r 的最大值不会很大,所以这里使用了基数排序。如果 r 的最大值很大, 那么把这段代码改成快速排序。代码:
for(i=0;i<m;i++) ws[i]=0; for(i=0;i<n;i++) ws[x[i]=r[i]]++; for(i=1;i<m;i++) ws[i]+=ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i; 这里 x 数组保存的值相当于是 rank 值。下面的操作只是用 x 数组来比较字 符的大小,所以没有必要求出当前真实的 rank 值。 接下来进行若干次基数排序,在实现的时候,这里有一个小优化。基数排序 要分两次,第一次是对第二关键字排序,第二次是对第一关键字排序。对第二关 键字排序的结果实际上可以利用上一次求得的 sa 直接算出,没有必要再算一次。 代码: for(p=0,i=n-j;i<n;i++) y[p++]=i; for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j; 其中变量 j 是当前字符串的长度,数组 y 保存的是对第二关键字排序的结果 。 然后要对第一关键字进行排序,代码: 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]; 这样便求出了新的 sa 值。在求出 sa 后,下一步是计算 rank 值。这里要注 意的是,可能有多个字符串的 rank 值是相同的,所以必须比较两个字符串是否 完全相同,y 数组的值已经没有必要保存,为了节省空间,这里用 y 数组保存 rank 值。这里又有一个小优化,将 x 和 y 定义为指针类型,复制整个数组的操作可以 用交换指针的值代替,不必将数组中值一个一个的复制。代码: for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
2.2.2 子串的个数 ……………………………………………………19 例 5:不相同的子串的个数(spoj694,spoj705)………………19
2.2.3 回文子串 ………………………………………………………19 例 6:最长回文子串(ural1297)…………………………………20
2.2.4 连续重复子串 …………………………………………………20 例 7:连续重复子串(pku2406)……………………………………20 例 8:重复次数最多的连续重复子串(spoj687,pku3693)………21
6
IOI2009 国家集训队论文
后缀数组 罗穗骞
具体实现: int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; int cmp(int *r,int a,int b,int l) {return r[a]==r[b]&&r[a+l]==r[b+l];} void da(int *r,int *sa,int n,int m) { int i,j,p,*x=wa,*y=wb,*t; for(i=0;i<m;i++) ws[i]=0; for(i=0;i<n;i++) ws[x[i]=r[i]]++; for(i=1;i<m;i++) ws[i]+=ws[i-1]; for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i; for(j=1,p=1;p<n;j*=2,m=p) { for(p=0,i=n-j;i<n;i++) y[p++]=i; for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j; 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]; for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } return; }
从字符串的大小比较的定义来看,S 的两个开头位置不同的后缀 u 和 v 进 行比较的结果不可能是相等,因为 u=v 的必要条件 len(u)=len(v)在这里不可 能满足。
后缀数组:后缀数组 SA 是一个一维数组,它保存 1..n 的某个排列 SA[1], SA[2],……,SA[n],并且保证 Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n。 也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺 次放入 SA 中。
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.1 基本定义
子串:字符串 S 的子串 r[i..j],i≤j,表示 r 串中从 i 到 j 这 一 段 , 也就是顺次排列 r[i],r[i+1],...,r[j]形成的字符串。
后缀:后缀是指从某个位置 i 开始到整个串末尾结束的一个特殊子串。字
4
IOI2009 国家集训队论文
后缀数组 罗穗骞
2.3 两个字符串的相关问题 …………………………………………………21 2.3.1 公共子串 ………………………………………………………22 例 9:最长公共子串(pku2774,ural1517) ………………………22 2.3.2 子串的个数 ……………………………………………………23
2
IOI2009 国家集训队论文
名次数组:名次数组 Rank[i]保存的是 Suffix(i)在所有后缀中从小到大排 列的“名次”。
简单的说,后缀数组是“排第几的是谁?”,名次数组是“你排第几?”。容 易看出,后缀数组和名次数组为互逆运算。如图 1 所示。
5
IOI2009 国家集训队论文
后缀数组 罗穗骞
设字符串的长度为 n。为了方便比较大小,可以在字符串后面添加一个字符, 这个字符没有在前面的字符中出现过,而且比前面的字符都要小。在求出名次数 组后,可以仅用 O(1)的时间比较任意两个后缀的大小。在求出后缀数组或名次 数组中的其中一个以后,便可以用 O(n)的时间求出另外一个。任意两个后缀如 果直接比较大小,最多需要比较字符 n 次,也就是说最迟在比较第 n 个字符时一 定能分出“胜负”。
后缀数组 罗穗骞
例 10:长度不小于 k 的公共子串的个数(pku3415) ……………23 2.4 多个字符串的相关问题 …………………………………………………23
例 11:不小于 k 个字符串中的最长子串(pku3294) ……………………24 例 12:每个字符串至少出现两次且不重叠的最长子串(spoj220)……24 例 13:出现或反转后出现在每个字符串中的最长子串(pku3294)……24 三、结束语 …………………………………………………………………………25 参考文献 ……………………………………………………………………………25 致谢 …………………………………………………………………………………25
3
IOI2009 国家集训队论文
后缀数组 罗穗骞
后缀数组
----处理字符串的有力工具
【摘要】
后缀数组是处理字符串的有力工具。后缀数组是后缀树的一个非常精巧的 替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也 并不逊色,而且它比后缀树所占用的内存空间小很多。可以说,在信息学竞赛中 后缀数组比后缀树要更为实用。本文分两部分。第一部分介绍两种构造后缀数组 的方法,重点介绍如何用简洁高效的代码实现,并对两种算法进行了比较。第二 部分介绍后缀数组在各种类型题目中的具体应用。