近来一直想学习后缀数组,找到一种高效的实现方便的后缀数组算法,于是自然找到国家集训队论文-《后缀数组——处理字符串的有力工具》,内容大致可以看懂,但是由于算法基础以及读识代码的能力确实有限,一直没能看懂里面的2倍增算法。2倍增算法本身并不难理解,但是罗穗骞那个简洁高效的代码着实读起来很是吃力。见贤思齐焉,这几天一定要读懂!!!
首先我来描叙一下后缀数组的2倍增算法。来看看几个相关概念;
1.子串:字符串中连续的一段;
2.后缀:最后一个字符为整个字符串末尾字符的特殊字串;
3.后缀数组:sa[]是某个1-n的排列,具体说来是将给定字符串的n个后缀按照字典序从小到大排列后依次将该后缀对应的起始位置存入sa[]中,保证suffix(sa[i])<suffix(sa[i+1]);
4.名次数组:rank[]是将从开始位置起的n个后缀的名次依次存储,与后缀数组互逆;
注意后缀数组sa[]表示排第几的是谁,名次数组rank[]表示谁排第几。注意区别体会。
2倍增算法的描述:
用倍增思想结合基数排序,对从每个位置开始的2^k个字符进行排序,求出排名rank[]并维护sa[],然后k++,对从每个位置开始的2^(k+1)个字符进行排序,求出排名rank[]并维护sa[],…以此类推,直到2^k大于n。求从某个位置开始的2^k个字符的排名rank[],这里是结合从该位置开始的2^k个字符的排名rank[]以及其后一个位置开始的2^k个字符的排名rank[],运用基数排序来解决的。
为了防止自己说错,下面引用原文中的描述:
倍增算法的主要思路是:用倍增的方法对每个字符开始的长度为2k 的子字符串进行排序,求出排名,即rank 值。k 从0 开始,每次加1,当2k 大于n 以后,每个字符开始的长度为2k 的子字符串便相当于所有的后缀。并且这些子字符串都一定已经比较出大小,即rank 值中没有相同的值,那么此时的rank 值就是最后的结果。每一次排序都利用上次长度为2k-1 的字符串的rank 值,那么长度为2k 的字符串就可以用两个长度为2k-1 的字符串的排名作为关键字表示,然后进行基数排序,便得出了长度为2k 的字符串的rank 值。
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; } 看似简单,但对我这样的初学者来说还是有一定的难度,不好读识清楚。 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;//将基数排序的桶清0 for(i=0;i<n;i++) ws[x[i]=r[i]]++;//x[i]=r[i],x[]存放1h的相对排名;ws[]存放每个桶中元素的个数;具体的每个ws[]存放并列排名的1hsa[] for(i=1;i<m;i++) ws[i]+=ws[i-1];//ws[]存放每个桶的累加排名 for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;//计算出每个元素的排名,相同桶中元素越靠后排名越大,保证1h排名从0-n-1,取逆建立后缀数组 for(j=1,p=1;p<n;j*=2,m=p) { //以下两行代码实现了对第二关键字的排序 for(p=0,i=n-j;i<n;i++) y[p++]=i;//后缀在原字符串中的起始位置在第n-j至n的元素的第二关键字都为0,因此如果按第二关键字排序,必然这些元素都是排在前面的。 for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;//if(sa[i]>=j),则说明该长度为j的片段可以与前面某个长度为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];//计算出每个元素的排名,相同桶中元素越靠后排名越大,保证1h排名从0-n-1,取逆建立后缀数组 for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)//这里就是用x[]存储计算出的各字符串rank的值了,记得我们前面说过,计算sa[]值的时候如果字符串相同是默认前面的更小的,但这里计算rank的时候必须将相同的字符串看作有相同的rank,要不然p==n之后就不会再循环啦 x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } return; }
以上是后缀数组的实现模板,即求出了sa[].但要想具体运用后缀数组,还得配合对应的height[]数组。下面是height[]数组的实现模板:
int rank[MAXN],height[MAXN]; //定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公 //共前缀,也就是排名相邻的两个后缀的最长公共前缀 //任意两个起始位置为i,j(假设rank[i]<rank[j])的后缀的最长公共前缀 //为height[rank[i]+1]、height[rank[i]+2]…height[rank[j]]的最小值 void calheight(char *r,int *sa,int n) { int i,j,k=0; for(i=1;i<=n;i++) rank[sa[i]]=i; 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; }
建立在height[]数组的基础上,后缀数组在字符串匹配等方面有着独特的优势。
求sa[]的时间复杂度为O(N*log(N));求height[]的时间复杂度为O(N),在要求算法高效的应用场合,二者都是可以接受的。
1.最长公共前缀
给定一个字符串,要求某两个后缀的最长公共前缀。
height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公/共前缀,也就是排名相邻的两个后缀的最长公共前缀。所求即为height[]数组的最大值,在先预处理的情况下时间复杂度为O(1)。对于RMQ问题,我们可以先求height[]数组的区间最值,即[l,r]区间内height[]的最小值。为什么是最小值呢?因为任意两个起始位置为i,j(假设rank[i]<rank[j])的后缀的最长公共前缀//为height[rank[i]+1]、height[rank[i]+2]…height[rank[j]]的最小值。RMQ问题可用ST算法在时间复杂度为O(N*log(N))内解决。
有一例题题解可供参考
http://blog.csdn.net/xj2419174554/article/details/10163209
2.可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串可以重叠。
任一子串必是某一后缀的前缀,最长重复字串也就是两个后缀的最长公共前缀。于是转化成了求height[]的最小值。
3.不可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串不能重叠。
不可重叠,就意味着不能简单地重复2.我们可以二分枚举子串的长度len,然后将height[]数组分段,下表连续且height[i]>=len的必在同一段,于是就是看该段的max(sa[i])-min(sa[j])是否大于等于len。若满足,则说明存在两个长度为len 的子串是相同的,且不重叠,此时就该右移而二分区间的下界;若不满足,就该左移而二分区间的上界。在于处理求出height[]的基础上,二分的时间复杂度为O(N*log(N))。
有一例题题解可供参考http://blog.csdn.net/xj2419174554/article/details/10171207
4.可重叠的k 次最长重复子串
给定一个字符串,求至少出现k 次的最长重复子串,这k 个子串可以重叠。
我们也可以像上题一样二分枚举子串的长度len,然后依据len对height[]数组分段,下表连续且height[i]>=len的必在同一段,于是就是看该段的该段内元素个数是否大于等于k-1。若满足,则说明存在这样的子串,此时就该右移而二分区间的下界;若不满足,就该左移而二分区间的上界。在于处理求出height[]的基础上,二分的时间复杂度为O(N*log(N))。
有一例题题解可供参考http://blog.csdn.net/xj2419174554/article/details/10175343
5.不相同的子串的个数
给定一个字符串,求不相同的子串的个数。
两个字符串的最长公共子串
给定两个字符串A 和B,求最长公共子串。
若分开讨论,可以有暴力和动态规划的思想可解此题。但时间复杂度为O(N*N),在n很大时一般是不可接受的。我们可以将两个字符串连接起来,在中间部分添加一个不在二者中出现的字符,将两个字符串分开。于是最长公共子串问题转化成了分别从两个区间开始的某两个后缀的最长公共前缀。于是转化成上面的了,只是注意起始位置一个在左一个在右。
有一例题题解可供参考http://blog.csdn.net/xj2419174554/article/details/9973723
下面几天我将细细完善我的这篇读书笔记。
在写这篇文章的时候也参考了这篇高人的读书笔记,在此一并表示感谢!
http://www.cnblogs.com/staginner/archive/2012/02/02/2335600.html