算法合集之《后缀数组》
构造后缀数组的DC3算法实现
构造后缀数组的DC3算法实现DC3算法(Difference Cover mod 3)是J. Kärkkäinen和P. Sanders在2003年发表的论⽂ "Simple Linear Work Suffix Array Construction"中描述的线性时间内构造后缀数组的算法。
相对Prefix Doubling(前缀倍增)算法⽽⾔,虽然它的渐进时间复杂度⽐较⼩,但是常数项⽐较⼤。
DC3算法的思想类似于找中位数的median of medians算法(/wiki/Selection_algorithm),它采⽤分治思想: 先⽤递归⽅式对起始下标等于1(mod 3)和2(mod 3)的后缀排序,从⽽将原始的后缀集合⼤⼩缩⼩为2/3,设这些后缀排好序的结果为S12,然后在S12的基础上对起始下标等于0(mod 3)的后缀排序(这⼀步只需作两位数的基数排序,⼀位为0(mod 3)的起始下标,另外⼀位为S12的rank 值),设这⼀步得到的排好序的后缀数组为S0,最后将S0和S12归并(类似于归并排序算法)。
归并过程通过 Difference Cover思想,也是在S12已知的基础上分两个cases得出相邻两个后缀的先后顺序。
实现:/**** Build Suffix Array using DC3/KS Algorithm*** Copyright (c) 2011 ljs (/ljsspace/)* Licensed under GPL (/licenses/gpl-license.php)** @author ljs* 2011-07-18**/public class DC3 {public static final char MAX_CHAR = '\u00FF';class Suffix{int[] sa;//Note: the p-th suffix in sa: SA[rank[p]-1]];//p is the index of the array "rank", start with 0;//a text S's p-th suffix is S[p..n], n=S.length-1.int[] rank;boolean done;public Suffix(int[] sa,int[] rank){this.sa = sa;this.rank = rank;}}//a prefix of suffix[isuffix] represented with digitsclass Tuple{int isuffix; //the p-th suffixint[] digits;public Tuple(int suffix,int[] digits){this.isuffix = suffix;this.digits = digits;}public String toString(){StringBuffer sb = new StringBuffer();sb.append(isuffix);sb.append("(");for(int i=0;i<digits.length;i++){sb.append(digits[i]);if(i<digits.length-1)sb.append("-");}sb.append(")");return sb.toString();}}//d: the digit to do countingsort//max: A value's range is 0...maxprivate void countingSort(int d,Tuple[] tA,Tuple[] tB,int max){//init the counter arrayint[] C = new int[max+1];for(int i=0;i<=max;i++){C[i] = 0;}//stat the countfor(int j=0;j<tA.length;j++){C[tA[j].digits[d]]++;}//process the counter array Cfor(int i=1;i<=max;i++){C[i]+=C[i-1];}//distribute the valuesfor(int j=tA.length-1;j>=0;j--){//C[A[j]] <= A.lengthtB[--C[tA[j].digits[d]]]=tA[j];}}//tA: input//tB: output for rank caculationprivate void radixSort(Tuple[] tA,Tuple[] tB,int max,int digitsLen){int len = tA.length;int digitsTotalLen = tA[0].digits.length;for(int d=digitsTotalLen-1,j=0;j<digitsLen;d--,j++){this.countingSort(d, tA, tB, max);//assign tB to tAif(j<digitsLen-1){for(int i=0;i<len;i++){tA[i] = tB[i];}}}}//max is the maximum value in any digit of TA.digits[], used for counting sort //tA: input//tB: the place holder, reused between iterationsprivate Suffix rank(Tuple[] tA,Tuple[] tB,int max,int digitsLen){int len = tA.length;radixSort(tA,tB,max,digitsLen);int digitsTotalLen = tA[0].digits.length;//caculate rank and saint[] sa = new int[len];sa[0] = tB[0].isuffix;int[] rank = new int[len+2]; //add 2 for sentinelrank[len]=1;rank[len+1] = 1;int r = 1; //rank starts with 1rank[tB[0].isuffix] = r;for(int i=1;i<len;i++){sa[i] = tB[i].isuffix;boolean equalLast = true;for(int j=digitsTotalLen-digitsLen;j<digitsTotalLen;j++){if(tB[i].digits[j]!=tB[i-1].digits[j]){equalLast = false;break;}}if(!equalLast){r++;}rank[tB[i].isuffix] = r;}Suffix suffix = new Suffix(sa,rank);//judge if we are doneif(r==len){suffix.done = true;}else{suffix.done = false;}return suffix;}private int[] orderSuffixes(Tuple[] tA,Tuple[] tB,int max,int digitsLen){int len = tA.length;radixSort(tA,tB,max,digitsLen);//caculate rank and saint[] sa = new int[len];for(int i=0;i<len;i++){sa[i] = tB[i].isuffix;}return sa;}//rank needs sentinel: len+2public Suffix reduce(int[] rank,int max){int len = rank.length - 2;int n1 = (len+1)/3;Tuple[] tA = new Tuple[n1+n2];Tuple[] tB = new Tuple[n1+n2];for(int i=0,j=1;i<n1;i++,j+=3){int r1 = rank[j];int r2 = rank[j+1];int r3 = rank[j+2];tA[i] = new Tuple(i,new int[]{r1,r2,r3});}for(int i=n1,j=2;i<n1+n2;i++,j+=3){int r1 = rank[j];int r2 = rank[j+1];int r3 = rank[j+2];tA[i] = new Tuple(i,new int[]{r1,r2,r3});}return rank(tA,tB,max,3);}public int[] skew(int[] rank,int max){int len = rank.length - 2;//step 1: caculate sa12Suffix suffixT12 = reduce(rank,max);int[] sa12 = null;if(!suffixT12.done){int[] rankT12 = suffixT12.rank;int maxT12 = rankT12[suffixT12.sa[suffixT12.sa.length-1]]; sa12 = skew(rankT12,maxT12);// debug for string: GACCCACCACC#//s12 = new Suffix();//s12.rank = new int[]{3,6,5,4,7,2,1,1,1};//s12.sa = new int[]{7,6,5,0,3,2,1,4};//s12.done =true;}else{sa12 = suffixT12.sa;}//index conversion for sa12int n1 = (len+1)/3;for(int j=0;j<sa12.length;j++){if(sa12[j]<n1){sa12[j] = 1 + 3*sa12[j];}else{sa12[j] = 2 + 3*(sa12[j]-n1);}}//recaculate rank for sa12int[] rank12 = new int[len+2];rank12[len] = 1;rank12[len+1] = 1;for(int k=0;k<sa12.length;k++){rank12[sa12[k]] = k+1;}//step 2: caculate sa0int n0=(len+2)/3;Tuple[] tA = new Tuple[n0];Tuple[] tB = new Tuple[n0];for(int i=0,j=0;i<n0;i++,j+=3){int r1 = rank[j];int r2 = rank12[j+1];tA[i] = new Tuple(i,new int[]{r1,r2});}int max12 = rank12[sa12[sa12.length-1]];int[] sa0 = orderSuffixes(tA,tB,max<max12?max12:max,2); //index conversion for sa0for(int j=0;j<n0;j++){sa0[j] = 3*sa0[j];}//step 3: merge sa12 and sa0int[] sa = new int[len];int i=0,j=0;int k=0;while(i<sa12.length && j<sa0.length){int p = sa12[i];int q = sa0[j];if(p%3==1){if(rank[p]<rank[q]){sa[k++] = p;i++;}else if(rank[p]>rank[q]){sa[k++] = q;j++;}else{if(rank12[p+1]<rank12[q+1]){sa[k++] = p;i++;}else{sa[k++] = q;j++;}}}else{//case 2if(rank[p]<rank[q]){sa[k++] = p;i++;}else if(rank[p]>rank[q]){sa[k++] = q;j++;}else{if(rank[p+1]<rank[q+1]){sa[k++] = p;i++;}else if(rank[p+1]>rank[q+1]){sa[k++] = q;j++;}else{if(rank12[p+2]<rank12[q+2]){sa[k++] = p;i++;}else{sa[k++] = q;j++;}}}}}for(int m=i;m<sa12.length;m++){sa[k++] = sa12[m];}for(int m=j;m<sa0.length;m++){sa[k++] = sa0[m];}return sa;}//Precondition: the last char in text must be less than other chars. public Suffix solve(String text){if(text == null)return null;int len = text.length();if(len == 0) return null;char base = text.charAt(len-1); //the smallest charTuple[] tA = new Tuple[len];Tuple[] tB = new Tuple[len]; //placeholderfor(int i=0;i<len;i++){tA[i] = new Tuple(i,new int[]{0,text.charAt(i)-base});}Suffix suffix = rank(tA,tB,MAX_CHAR-base,1);int max = suffix.rank[suffix.sa[len-1]];int[] sa = skew(suffix.rank,max);//caculate rank for result suffix arrayint[] r = new int[len];for(int k=0;k<sa.length;k++){r[sa[k]] = k+1;}return new Suffix(sa,r);}public void report(Suffix suffix){int[] sa = suffix.sa;int[] rank = suffix.rank;int len = sa.length;System.out.println("suffix array:");for(int i=0;i<len;i++){System.out.format(" %s", sa[i]);}System.out.println();System.out.println("rank array:");for(int i=0;i<len;i++){System.out.format(" %s", rank[i]);}System.out.println();}public static void main(String[] args) {String text = "GACCCACCACC#";DC3 dc3 = new DC3();Suffix suffix = dc3.solve(text);System.out.format("Text: %s%n",text);dc3.report(suffix);text = "mississippi#";dc3 = new DC3();suffix = dc3.solve(text);System.out.format("Text: %s%n",text);dc3.report(suffix);text = "abcdefghijklmmnopqrstuvwxyz#";dc3 = new DC3();suffix = dc3.solve(text);System.out.format("Text: %s%n",text);dc3.report(suffix);text = "yabbadabbado#";dc3 = new DC3();suffix = dc3.solve(text);System.out.format("Text: %s%n",text);dc3.report(suffix);text = "DFDLKJLJldfasdlfjasdfkldjasfldafjdajfdsfjalkdsfaewefsdafdsfa#";dc3 = new DC3();suffix = dc3.solve(text);System.out.format("Text: %s%n",text);dc3.report(suffix);}}测试:Text: GACCCACCACC#suffix array:11 8 5 1 10 7 4 9 6 3 2 0rank array:12 4 11 10 7 3 9 6 2 8 5 1Text: mississippi#suffix array:11 10 7 4 1 0 9 8 6 3 5 2rank array:6 5 12 10 4 11 9 3 87 2 1Text: abcdefghijklmmnopqrstuvwxyz#suffix array:27 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26rank array:2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 1Text: yabbadabbado#suffix array:12 1 6 4 9 3 8 2 7 5 10 11 0rank array:13 2 8 6 4 10 3 9 7 5 11 12 1Text: DFDLKJLJldfasdlfjasdfkldjasfldafjdajfdsfjalkdsfaewefsdafdsfa#suffix array:60 0 2 1 5 7 4 6 3 59 47 54 30 34 41 17 11 25 53 29 33 9 19 23 13 56 44 37 50 48 58 46 10 55 36 39 15 31 20 27 51 40 16 24 32 35 43 21 28 8 22 14 42 52 18 12 57 45 38 26 49rank array:2 43 9 7 5 8 6 50 22 33 17 56 25 52 37 43 16 55 23 39 48 51 24 44 18 60 40 49 20 13 38 45 21 14 46 35 28 59 36 42 15 53 47 27 58 32 11 30 61 29 41 54 19 12 34 26 57 31 10 1。
【字符串】后缀数组
【字符串】后缀数组后缀排序倍增算法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循环移动,找字典序最⼩的那个表⽰。
后缀数组实现的倍增算法和DC3算法
后缀数组实现的倍增算法和DC3算法[cpp] view plaincopyprint?/************************************************数据结构:后缀数组(Suffix_Array);子串:字符串S的子串r[i..j],i≤j,表示r串中从i到j这一段,也就是顺次排列r[i],r[i+1],...,r[j]形成的字符串;后缀:后缀是指从某个位置i开始到整个串末尾结束的一个特殊子串;字符串r的从第i个字符开始的后缀表示为Suffix(i),也就是Suffix(i)=r[i...len(r)];后缀数组SA:后缀数组保存的是一个字符串的所有后缀的排序结果;其中SA[i]保存的是字符串所有的后缀中第i小的后缀的开头位置;名次数组Rank:名次数组Rank[i]保存的是后缀i在所有后缀中从小到大排列的“名次”;后缀数组是"排第几的是谁",名次数组是"排第几",即后缀数组和名次数组为互逆运算;(1)倍增算法:用倍增的方法对每个字符开始的长度为2^k的子字符串进行排序,求出排名,即rank值。
k从0开始,每次加1,当2^k大于n以后,每个字符开始的长度为2^k的子字符串便相当于所有的后缀。
并且这些子字符串都一定已经比较出大小,即rank值中没有相同的值,那么此时的rank值就是最后的结果。
每一次排序都利用上次长度为2^k-1的字符串的rank值,那么长度为2^k的字符串就可以用两个长度为2^k-1的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为2^k的字符串的rank值。
(2)DC3算法:①先将后缀分成两部分,然后对第一部分的后缀排序;②利用①的结果,对第二部分的后缀排序;③将①和②的结果合并,即完成对所有后缀排序;时间复杂度:倍增算法的时间复杂度为O(nlogn),DC3算法的时间复杂度为O(n);从常数上看,DC3算法的常数要比倍增算法大;空间复杂度:倍增算法和DC3算法的空间复杂度都是O(n);倍增算法所需数组总大小为6n,DC3算法所需数组总大小为10n;RMQ(Range Minimum/Maximum Query)问题:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。
后缀树与后缀数组
• 显然, 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
后缀数组板子
后缀数组hdu6704题意:查询l-r之间的子串第k次出现的位置一个子串必定是某个后缀的前缀。
排序相邻的后缀他们的前缀一定最相似。
所以全部的一种子串必定是一些排序相邻的后缀的公共前缀。
从l开始的子串,则从rank[l]开始看,两侧height保证大于子串长度,能延伸多长,则证明有多少个这种子串。
我们用ST表维护出height的最小值,然后通过最小值二分即可,边界有些棘手。
然后我们就得到了一个height不小于子串长度的连续区间,这个区间是以原后缀的字典序排序的。
而同时,sa数组下标为排序,值为原串位置。
所以我们对这个区间在sa数组上做主席树,求第k大,即为第k个子串出现的位置#include<bits/stdc++.h>using namespace std;typedef long long ll;const int INF=0x3f3f3f3f;const int N=1e5+5;char s[N];int sa[N],t[N],t2[N],c[N],n,m;//c是一个桶,sa是后缀数组,sa[i]表示排名第i的是第几个串//x表示rank,x[i]表示第i个串的排名,也是第一关键字//y[i]表示第二关键字排名第i的是第几个串int height[N],rk[N],d[N][30];int root[N],cnt;struct node{int l,r,sum;}T[N*40];void build_sa(int m){int i,*x=t,*y=t2;//基数排序for(int i=1;i<=m;i++)c[i]=0;//清空桶for(i=1;i<=n;i++){//一开始的rank为第一个字符的大小,可以用ascii码表示,并加入桶c[x[i]=s[i]]++;}for(i=2;i<=m;i++){//前缀和,c[i]可以表示前面有多少个比他大c[i]+=c[i-1];}for(i=n;i>=1;i--){sa[c[x[i]]--]=i;//更新sa}for(int k=1;k<=n;k<<=1)//通过第一位依次倍增出每一位{int p=0;//直接利用sa数组排序第二关键字for(i=n-k+1;i<=n;i++){y[++p]=i;//第n-k到第n个串没有后面的后缀,排在最前面}for(i=1;i<=n;i++){//加入排名第i的是第j个串,此时这个串就是j-k个串的右半部分,也就是第二关键字的排名直接可以通过sa获得if(sa[i]>k)y[++p]=sa[i]-k;}//基数排序第一关键字for(i=1;i<=m;i++)c[i]=0;for(i=1;i<=n;i++)c[x[y[i]]]++;for(i=2;i<=m;i++)c[i]+=c[i-1];for(i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];//通过先把第二关键字大的先搞出来,可以实现双关键字排序}//根据sa和y数组计算新的x数组swap(x,y);p=1;x[sa[1]]=1;//通过sa更新x,其中如果两个串完全相同,那么rank,也就是x也相同for(i=2;i<=n;i++){x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]?p:++p;}if(p>=n)break;m=p;}}void get_Height(){int i,j,k=0;for(i=1;i<=n;i++){rk[sa[i]]=i;}for(i=1;i<=n;i++){if(rk[i]==1)continue;if(k)k--;j=sa[rk[i]-1];while(s[i+k]==s[j+k]&&(i+k<=n)&&(j+k<=n))k++;height[rk[i]]=k;}}void RMQ_init(){for(int i=1;i<=n;i++)d[i][0]=height[i];for(int j=1;(1<<j)<=n;j++){for(int i=1;i+(1<<j)-1<=n;i++){d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);}}}int RMQ(int L,int R){int k=0;while((1<<(1+k))<=R-L+1) k++;return min(d[L][k],d[R-(1<<k)+1][k]);}int LCP(int x,int y){int l=rk[x],r=rk[y];if(l>r)swap(l,r);if(l==r)return n-sa[l];return RMQ(l+1,r);}int getL(int l,int r){int left=1,right=rk[l],len=r-l+1;int ans=rk[l];while(left<=right){int mi=(left+right)>>1;if(LCP(sa[mi],l)<len)left=mi+1;else{right=mi-1;ans=mi;}}return ans;}int getR(int l,int r){int ans=rk[l];int left=rk[l],right=n,len=r-l+1;while(left<=right){int mi=(left+right)>>1;if(LCP(sa[mi],l)<len){right=mi-1;}else{left=mi+1;ans=mi;}}return ans;}void update(int l,int r,int pre,int &now,int pos) {T[++cnt]=T[pre];now=cnt;T[cnt].sum++;if(l==r)return;int m=(l+r)>>1;if(pos<=m)update(l,m,T[pre].l,T[now].l,pos);else update(m+1,r,T[pre].r,T[now].r,pos);}int query(int l,int r,int pre,int now,int k){if(l==r)return l;int m=(l+r)>>1;int sum=T[T[now].l].sum-T[T[pre].l].sum;if(k<=sum)return query(l,m,T[pre].l,T[now].l,k);else return query(m+1,r,T[pre].r,T[now].r,k-sum);}int main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);int t,q;m='z'+1;cin>>t;while(t--){cin>>n>>q;cin>>s+1;build_sa('z'+1);get_Height();RMQ_init();cnt=0;for(int i=1;i<=n;i++){update(1,n,root[i-1],root[i],sa[i]);}while(q--){int l,r,k;cin>>l>>r>>k;int L=getL(l,r);int R=getR(l,r);if(R-L+1<k)cout<<-1<<endl;else cout<<query(1,n,root[L-1],root[R],k)<<endl;}}}。
SuffixArray
为什么学后缀数组后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握。
学会后缀自动机(SAM)就不用学后缀数组(SA)了?不,虽然SAM看起来更为强大和全面,但是有些SAM解决不了的问题能被SA解决,只掌握SAM是远远不够的。
……而下面是我对后缀数组的一些理解构造后缀数组——SA先定义一些变量的含义Str:需要处理的字符串(长的为Len)Suffix[i] :Str下标为i ~ Len的连续子串(即后缀)Rank[i] : Suffix[i]在所有后缀中的排名SA[i] : 满足Suffix[SA[1]] < Suffix[SA[2]] …… < Suffix[SA[Len]],即排名为i的后缀为Suffix[SA[i]] (与Rank是互逆运算)好,来形象的理解一下后缀数组指的就是这个SA[i],有了它,我们就可以实现一些很强大的功能(如不相同子串个数、连续重复子串等)。
如何快速的到它,便成为了这个算法的关键。
而SA和Rank是互逆的,只要求出任意一个,另一个就可以O(Len)得到。
现在比较主流的算法有两种,倍增和DC3,在这里,就主要讲一下稍微慢一些,但比较好实现以及理解的倍增算法(虽说慢,但也是O(Len logLen))的。
进入正题——倍增算法倍增算法的主要思想:对于一个后缀Suffix[i],如果想直接得到Rank 比较困难,但是我们可以对每个字符开始的长度为2k的字符串求出排名,k从0开始每次递增1(每递增1就成为一轮),当k大于Len时,所得到的序列就是Rank,而SA也就知道了。
O(logLen)枚举k这样做有什么好处呢?设每一轮得到的序列为rank(注意r是小写,最终后缀排名Rank是大写)。
有一个很美妙的性质就出现了!第k轮的rank可由第k - 1轮的rank快速得来!为什么呢?为了方便描述,设SubStr(i, len)为从第i个字符开始,长度为len的字符串我们可以把第k轮SubStr(i,2k)看成是一个由SubStr(i,2k−1)和SubStr(i +2k−1,2k−1)拼起来的东西。
窗外一叶?后缀树与后缀数组
窗外一叶?后缀树与后缀数组本文系转载:/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 上搜到并下载的。
后缀数组之倍增算法
后缀数组之倍增算法⾸先说明:后缀数组的构建在⽹上有多种⽅法:朴素的n*n*logn,还有倍增n*logn的,还有3*n的DC3算法,当然还有DC算法。
这个算法学习⾃林厚丛⽼师的《⾼级数据结构》,代码较长,⽽且常数也⽐较⼤,但是是我这种笨⼈可以理解的。
如有⼈想学短⽽快的可以学习《罗穗骞后缀数组 ---处理字符串的有⼒⼯具》。
顺便说⼀下,罗⼤神的算法书写的的确很短⼩也漂亮,可惜我看不懂。
说⼀下学习的⼼路历程吧!最开始想学后缀树,道理看明的了,可是⼀看代码实在是太长了(可能是我找的模版不对吧)。
后来看到后缀数组的功能也不错,可以实现后缀树的很多功能,于是转向后缀数组。
于是向林⼤神学习,可是在他漂亮的代码映照下的是我愚笨的脑袋,最后是林厚丛⽼师救了我。
感谢林⽼师学习前的准备:1、后缀数组的各种基本概念 后缀:字符串中从第i个开始到它的最后⼀个。
如字符串abcde。
则bcde、cde、de、e都是他的后缀,当然他本⾝也是⾃⼰的后缀。
后缀数组:有两种sa数组和rank数组。
sa[i]表⽰把字符串的所有后缀排序后排第i的是以第⼏个字母开头的后缀。
rank[i]表⽰以第i个字母开头的后缀在后缀的排序中排第⼏。
2、计数排序和基数排序(可以百度⼀下) 计数排序也就是桶排,时间复杂度O(n)1 #include <iostream>2using namespace std;3const int MAXN = 100000;4const int k = 1000; // range5int a[MAXN], c[MAXN], ranked[MAXN];67int main() {8int n;9 cin >> n;10for (int i = 0; i < n; ++i) {11 cin >> a[i];12 ++c[a[i]];13 }14for (int i = 1; i < k; ++i)15 c[i] += c[i-1];16for (int i = n-1; i >= 0; --i)17 ranked[--c[a[i]]] = a[i];//如果是i表达的是原数标号,a[i]就是排序后的正确序列18for (int i = 0; i < n; ++i)19 cout << ranked[i] << endl;20return0;21 }View Code 基数排序,也称桶⼦排序(注意和上⾯的区分),实际上是分关键安排序。
最长公共子串问题的后缀数组解法
最长公共⼦串问题的后缀数组解法[最长公共⼦串]最长公共⼦串(Longest Common Substring ,简称LCS)问题,是指求给定的⼀组字符串长度最⼤的共有的⼦串的问题。
例如字符串”abcb”,”bca”,”acbc”的LCS就是”bc”。
求多串的LCS,显然穷举法是极端低效的算法。
改进⼀些的算法是⽤⼀个串的每个后缀对其他所有串进⾏部分匹配,⽤KMP算法,时间复杂度为O(N*L^2),其中N为字符串个数,L为每个串的长度。
更优秀的有⼴义后缀树的⽅法,时间可以达到 O(N*L)。
本⽂介绍⼀种基于后缀数组的LCS解法,利⽤⼆分查找技术,时间复杂度可以达到O(N*L*logL)。
[最长公共⼦串问题的后缀数组解法]关于后缀数组的构建⽅法以及Height数组的性质,本⽂不再具体介绍,可以参阅IOI国家集训队2004年论⽂《后缀数组》(许智磊)和IOI国家集训队2009年论⽂《后缀数组——处理字符串的有⼒⼯具》(罗穗骞)。
回顾⼀下后缀数组,SA[i]表⽰排名第i的后缀的位置,Height[i]表⽰后缀SA[i]和SA[i-1]的最长公共前缀(Longest Common Prefix,LCP),简记为Height[i]=LCP(SA[i],SA[i-1])。
连续的⼀段后缀SA[i..j]的最长公共前缀,就是H[i-1..j]的最⼩值,即LCP(SA[i..j])=Min(H[i-1..j])。
求N个串的最长公共⼦串,可以转化为求⼀些后缀的最长公共前缀的最⼤值,这些后缀应分属于N个串。
具体⽅法如下:设N个串分别为S1,S2,S3,…,SN,⾸先建⽴⼀个串S,把这N个串⽤不同的分隔符连接起来。
S=S1[P1]S2[P2]S3…SN-1[PN-1]SN,P1,P2,…PN-1应为不同的N-1个不在字符集中的字符,作为分隔符(后⾯会解释为什么)。
接下来,求出字符串S的后缀数组和Height数组,可以⽤倍增算法,或DC3算法。
后缀数组题型讲解
后缀数组、不重复子串Distinct Substrings题目大意:给出一个字符串,问它的不重复子串有多少个。
两题是一样的,除了字符串长度..分析:用后缀数组可以轻松解决。
因为这个字符串的每个子串必然是某个后缀的前缀,先用后缀数组求出sa和height,那么对于sa[k],它有n-sa[k]个子串,其中有height[k]个是和上一个后缀重复的,所以要减去。
所以用后缀数组求解的时间复杂度是O(n),后缀数组要是用倍增算法是O(nlog2n),效率很高。
note:wa了一次,主要原因是忘了a[n]=0这个关键的初值...PS:各位大牛对我的差劲的c++代码有什么看法可以尽管喷哈!codes:#include<iostream>#include<cstring>using namespace std;const long maxn=1010;long wn[maxn],wa[maxn],wb[maxn],wv[maxn],a[maxn],sa[maxn],rank[maxn],height[maxn]; char r[maxn];long cmp(long *r,long a,long b,long l){return r[a]==r[b]&&r[a+l]==r[b+l];}void da(long *r,long *sa,long n,long m){long i,j,p,*x=wa,*y=wb,*t;for (i=0;i<m;i++) wn[i]=0;for (i=0;i<n;i++) wn[x[i]=r[i]]++;for (i=1;i<m;i++) wn[i]+=wn[i-1];for (i=n-1;i>=0;i--) sa[--wn[x[i]]]=i;for (p=1,j=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<m;i++) wn[i]=0;for (i=0;i<n;i++) wn[wv[i]=x[y[i]]]++;for (i=1;i<m;i++) wn[i]+=wn[i-1];for (i=n-1;i>=0;i--) sa[--wn[wv[i]]]=y[i];for (t=x,x=y,y=t,x[sa[0]]=0,p=1,i=1;i<n;i++)x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;}return;}void calheight(long *r,long *sa,long n){long i,j,k=0;for (i=1;i<=n;i++) {rank[sa[i]]=i;height[i]=0;}for (i=0;i<n;height[rank[i++]]=k)for (k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);return;}int main(){long t,i;cin >> t;while (t--){cin >> r;long n=strlen(r);for (int i=0;i<n;i++) a[i]=static_cast<int>(r[i]);a[n]=0;da(a,sa,n+1,256);calheight(a,sa,n);long sum=0;for (i=1;i<=n;i++) sum+=n-sa[i]-height[i];cout << sum << endl;}return 0;}----------------------------------------------------------------------------------------------------------------------------[后缀数组]最长重复子串分析:任何一个重复子串,必然是某两个后缀的公共前缀。
后缀数组DC3构造法——详解
后缀数组DC3构造法——详解 学习了后缀数组,顺便把DC3算法也看了⼀下,传说中可以O(n)复杂度求出⽂本串的height,先⽐较⼀下倍增算法和DC3算法好辣。
DC3 倍增法时间复杂度 O(n)(但是常数很⼤) O(nlogn)(常数较⼩)空间复杂度 O(n) O(n) 编程复杂度较⾼ 较低 由于在时间复杂度上DC3的常数⽐较⼤,再加上编程复杂度⽐较⾼,所以在解决问题的时候并不是最优选择。
但是学到了后缀数组还是补充⼀下的好点。
DC3算法的实现: 1:先把⽂本串的后缀串分成两部分,第⼀部分是后缀串i mod 3 == 0, 第⼆部分是i mod 3 != 0,然后先⽤基数排序对第⼆部分后缀串排序(按照前三个字符进⾏排序)。
1int *san = sa+n, *rn = r+n, ta=0, tb=(n+1)/3, tbc=0, i, j, p;2//ta i mod 3==0的个数,tb i mod 3==1的个数, tbc imod3!=0的个数3for (i=0; i<n; i++)4if (i % 3)5 x[tbc ++] = i;67 r[n] = r[n+1] = 0;//在⽂本串后⾯添加两个0,便于处理8 Sort (r+2, x, y, tbc, m);9 Sort (r+1, y, x, tbc, m);10 Sort (r, x, y, tbc, m); 然后把suffix[1]与suffix[2]数组连起来,每三个相邻的字符看做⼀个数,变成这个样⼦:操作代码如下:1 rn[F(y[0])] = 0;2for (i=1, p=1; i<tbc; i++)3 rn[F(y[i])] = c0(r, y[i-1], y[i])?p-1:p++;4//#define F(x) x/3+(x%3==1?0:tb)5//F(x) 求原字符串suffix(i)在新串中的位置如果p>=tbc的话,也就是说只排列前三个字符就可以区分出第⼆部分后缀串的顺序了,否则就要进⾏递归继续对第⼆部分的串进⾏排序。
后缀数组的定义及实现
后缀数组的定义及实现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。
字符串匹配(三)----后缀数组算法
字符串匹配(三)----后缀数组算法⼀、什么是后缀数组: 字符串后缀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
Java后缀数组-求sa数组
Java后缀数组-求sa数组后缀数组的⼀些基本概念请⾃⾏百度,简单来说后缀数组就是⼀个字符串所有后缀⼤⼩排序后的⼀个集合,然后我们根据后缀数组的⼀些性质就可以实现各种需求。
1public class MySuffixArrayTest {23public char[] suffix;//原始字符串45public int n;//字符串长度67public int[] rank;// Suffix[i]在所有后缀中的排名89public int[] sa;// 满⾜Suffix[SA[1]] < Suffix[SA[2]] …… < Suffix[SA[Len]],即排名为i的后缀为Suffix[SA[i]]10// (与Rank是互逆运算)1112public int[] height;// 表⽰Suffix[SA[i]]和Suffix[SA[i - 1]]的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀1314public int[] h;// 等于Height[Rank[i]],也就是后缀Suffix[i]和它前⼀名的后缀的最长公共前缀1516public int[] ws;// 计数排序辅助数组1718public int[] y;// 第⼆关键字rank数组1920public int[] x;// rank的辅助数组21}以下的讲解都以"aabaaaab"这个字符串为例,先展⽰⼀下结果,请参考这个结果进⾏理解分析(这个结果图我复制别⼈的,请各位默认下标减1,因为我的数组从下标0开始的)suffix:原始字符串数组假设原始字符串是"aabaaaab" 那这个数组对应的值应该是{'a','a','b','a','a','a','a','b'}n:字符串长度 这⾥n是8rank: 后缀数组的名次数组相当于存的是第i个后缀对应的名次是多少⽐如rank[0]就是指"aabaaaab"这个后缀的名次 rank[1]指"abaaaab"这个后缀的名次sa: 这个是和rank数组互逆的⼀个数组存的是第x名的是哪个后缀还是举例⼦来说明 sa[0]指的是排名第⼀的后缀数组即为3 也就是"aaaab"这个数组他对应的rank[3]就是0。
详细解析后缀数组(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)。
后缀数组 VS 后缀树
后缀数组是直接针对G结ene论ral Alphabet设计的算法
对缀于树I甚nt至eg在er时和复间Ge杂复ne度杂ra跟度l以字上及符都|Σ集无|较的法大类胜的型过C没后on有缀st关数an系组t A。lphabet,后 后缀树则对不同字符集有不同的表现 但是对于|Σ|较小的Constant Alphabet,
后缀是一种特殊的子串 从某个位置i开始到整个串的末尾结束 S的从i开头的后缀等价于S[i..len(S)]
后缀数组——定义和符号
约定一个字符集Σ 待处理的字符串约定为S,约定len(S)=n 规定S以字符“$”结尾,即S[n]=“$”
“$”小于Σ中所有的字符 除了S[n]=“$”之外,S的其他字符都属于Σ
倍增定算义k法-前缀(D比o较u关bl系in<gk,A=kl和g≤okrithm)
对两个字符串u,v,
u<kv u=kv u≤kv
当且仅当 当且仅当 当且仅当
uk<vk uk=vk uk≤vk
后缀数组——构造方法
k
k
k
k
后缀u,以设i开u头=iS+ukffix后(i)缀,v,v=以Siu开f头fijx+(kj)
后缀数组!!!
后缀数组——应用举例
解法: 1. 初始化答案为0。按照前述方法修改串T,得到串S 2. 求出后缀数组SA、名次数组Rank 3. 计算height数组并进行标准RMQ方法预处理 4. 枚举i,计算以i为中心向两边扩展的最远值并更新答案 复杂度: 设len(S)=n,则n=2m+2
O(m) + O(nlogn) + 2*O(n) + m*O(1) = O(nlogn)
这是后缀数组
可以在常数时间内计算出
最常任用何以两个及后最缀强的最大长的公共功前能缀 之一
后缀数组——应用举例
怎样使用后缀数组?
后缀数组——应用举例
回文串——顺读和倒读完全一样的字符串
回文串
奇回文串 字符串u满足: 1. len(u)=p为奇数 2. 对任何1≤i≤(p-1)/2,u[i]=u[p-i+1]
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-r-1 r
i
r i+r+1
以某个位置为中心向两边扩展的复杂度为 反O射(m相) 等
整个算法的复杂度为
O(m2)
后缀数组——应用举例
我们已经在O(nlogn)的时间内构造出了
后缀数组SA 和 名次数组Rank
后缀数组——方法总结
利用到后缀之间的联系
用k-前缀比较关系来表达2k-前缀比较关系
倍
每次可以将参与比较的前缀长度加倍
增 思
根据SAk、Rankk求出SA2k、Rank2k
想
参与比较的前缀长度达到n以上时结束
后缀数组——辅助工具
符排序 O(nlogn) Rank1 O(n)
SA2 Rank2
O(n)
SA4 Rank4
O(n)
m=2t且m≥n
SAm Rankm
O(nlogn)的操作一次
O(n)的操作[logn]次
……
O(n)
SA8 O(n) Rank8
总复杂度为
O(nlogn)
后缀数组——构造方法
m≥n,
SAm=SA Rankm=Rank
偶回文串 字符串v满足: 1. len(v)=q为奇数 2. 对任何1≤i≤q/2,v[i]=v[q-i+1]
后缀数组——应用举例
字符串T的回文子串——T的子串,并且是回文串 字符串T的最长回文子串——T的回文子串中长度最大的
给出一个字符串T,求它的最长回文子串
给出最大长度即可 设len(T)=m
Suffix(SA[i]) Suffix(SA[i+1])
3=LCP(i+1,i+2)
Suffix(SA[i+2])
5=LCP(i+2,j)
Suffix(SA[j])
后缀数组——辅助工具
设height[i]=LCP(i-1,i) 根据LCP Theorem
LCP(i,j)=min{Байду номын сангаасeight[k] | i+1≤k≤j} 计算LCP(i,j)等价于
对于约定的字符串S,其i开头的后缀表示为
Suffix(i)
后缀数组——定义和符号
字符串的大小关系 按照通常所说的“字典顺序”进行比 较
我们对S的n个后缀按照字典顺序从小到大排序
将排序后的后缀的开头位置顺次放入数组SA中,称为
后缀数组
令Rank[i]保存Suffix(i)在排序中的名次,称数组Rank为
如果已经求出Rankk 可以在常数时间内对两个后缀进行k-前缀意义下的比较 可以在常数时间内对两个后缀进行2k-前缀意义下的比较
可以很方便地对所有的后缀在2k-前缀意义下排序 ➢采用快速排序O(nlogn) ➢采用基数排序O(n)
可以在O(n)时间内由Rankk求出SA2k 也就可以在O(n)时间内求出Rank2k
比较红色字符相当于在k-前缀意义下比较
在2k-前对缀u、意Sv义u在ff下2ikx比-(前i)较缀和两意个S义u后f下f缀ix进(可j行)以比转较化成
比较在绿k-色前字缀符意相义当下于比在k较-前两缀个意后义缀下比较
Suffix(i+k) 和 Suffix(j+k)
后缀数组——构造方法
把n个后缀按照k-前缀意义下的大小关系从小到大排序
名次数组
后缀数组——构造方法
如何构造后缀数组?
把n个后缀当作n个字符串,按照普通的方法进行排序 —— O(n2)
低效的原因 —— 把后缀仅仅当作普通的、独立 的字符串,忽略了后缀之间存在的有机联系。
后缀数组——构造方法
u[1..k] ,len(u)≥k 对字符串u,定义uk =
u
,len(u)<k
求以一个位置i为中心向两边扩展的最远值 是算法的核心部分
需要降低这一步的复杂度
后缀数组——应用举例
T串 中心 i
i'=2m-i+2
T'串
#
$
i-r
i+r
i'+r
求以i为中心向两边扩展的最远值,等价于
求SuffSixu(fif)i和x(iS同)u和f时fSixu和(fif'粉)ix的(红i最')串的长反公公共射共前相前缀等缀
询问数组height中下标从 i+1 到 j 范围内所有元素的最小值
经典的RMQ (Range Minimum Query)问题!!!
➢线段树、排序树 —— O(nlogn)预处理 , O(logn)每次询问 ➢标准RMQ方法 —— O(n)预处理 , O(1)每次询问
后缀数组——辅助工具
采用一种“神奇的”方法,可以在O(n)时间内计算出height数组 采用标准RMQ方法在O(n)时间内进行预处理 之后就可以在常数时间内算出任何的LCP(i,j)
将排序后的后缀的开头位置顺次放入数组SAk中,称为
k-后缀数组
用Rankk[i]保存Suffix(i)在排序中的名次,称数组Rankk为
k-名次数组
后缀数组——构造方法
利用SAk可以在O(n)时间内求出Rankk 利用Rankk可以在常数时间内对两个 后缀进行k-前缀意义下的大小比较
后缀数组——构造方法
后缀数组——最后的话
研究后缀数组,不是因为害怕后缀树的繁琐 也没有贬低后缀树,抬高后缀数组的意思 对于功能相似的两个数据结构,我们应 该灵活地掌握,有比较有选择地使用 构造后缀数组用到的倍增思想对我们的思考也是有帮助的
后缀数组
后缀数组——构造方法
1-前缀比较关系实际上是对字符串的第一个字符进行比较 可以直接根据开头字符对所有后缀进行排序求出SA1 ➢采用快速排序,复杂度为O(nlogn) 然后根据SA1在O(n)时间内求出Rank1
可以在O(nlogn)时间内求出SA1和Rank1
后缀数组——构造方法
直接根 据首字
SA1
后缀数组 VS 后缀树
后缀树也可以做到类似的事情
后缀数组有什么优势呢?
后缀数组 VS 后缀树
后缀数组在信息学竞赛中最大的优势:
易于理解,易于编程,易于调试
后缀数组比后缀树占用的空间少
——处理长字符串,如DNA分析
后缀数组 VS 后缀树
时间复杂度的比较
按照字符总数|Σ|把字符集Σ分为三种类型: Constant Alphabet —— |Σ|是一个常数 Integer Alphabet —— |Σ|为字符串长度n的多项式函数 General Alphabet —— 对|Σ|没有任何限制 Constant Alphabet Integer Alphabet General Alphabet
仅仅靠后缀数组和名次数组 有时候还不能很好地处理问题
后缀数组的最佳搭档——LCP
定义两个字符串的最长公共前缀Longest Common Prefix lcp(u,v)=max{i|u=iv}