后缀数组
作者:dylantsou
出处:http://blog.csdn.net/dylantsou
首先看一个问题,查找一个字符串中的最大回文子串。例如:S ADAMSMADY,它的最大回文子串就是DAMSMAD。
这道题目的解法如下,先构造它的反转字符串S‘ YDAMSMADA,那么求回文问题也就转换成了求字符串S与S’的最大公共子串问题,这个题目可以通过列出S与S'的所有后缀,在他们中找最长公共前缀的方法来解决。可以用后缀树来实现,也可以通过后缀数组+LCS来实现,后者原理简单其实起来比较容易,本文重点阐述其方法。
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=0;i--) sa[--ws[x[i]]]=i;
for(j=1,p=1;p=j) y[p++]=sa[i]-j;
for(i=0;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
通过这张图片,说明了程序运行的过程。
在第6-9行中
for(i=0;i=0;i--) sa[--ws[x[i]]]=i; //得到了sa数组,也就是1-前缀的SA数组
在上面的代码中,对于m的理解,m是不相同的字母的个数。
这四行代码实现了对一个字母的基数排序,其中x[]就是1-前缀 Rank数组,在第8行中,ws[i]表示在字母表中第i个字母前面的所有字母,在r中出现的总次数,所有ws[m]一定等于n的,因为m是最大的字母,而小于它的字母包括了所有的字母,一共为n个。ws[]数组保证了,字典顺序越大的字母,其ws值也就越大。通过倒序输出所有的值,也就为所有字母排序了,用aabaaaab来说明:
n = 8 x[0]= a x[1] = a x[2] =b x[3] = a x[4] = a x[5]= a x[6] = a x[7] = b
ws[a] = 6 ws[b] = 8
i = 7 x[7] = b ws[b] = 7 sa[7] = 7
i = 6 x[6] = a ws[a] = 5 sa[5] = 6
i = 5 x[5] = a ws[a] = 4 sa[4] = 5
i = 4 x[4] = a ws[a] = 3 sa[3] = 4
i = 3 x[3] = a ws[a] = 2 sa[2] = 3
i = 2 x[2] = b ws[b] = 6 sa[6] = 2
i = 1 x[1] = a ws[a] = 1 sa[1] = 1
i = 0 x[0] = a ws[a] = 0 sa[0] = 0
在10行的循环 for(j=1,p=1;p
第10行循环中的j也就是公式中的k。在第十行的循环里面,是用x[]来表示Rankk的,所以第一个关键字是x[i],第二个关键字是x[i+j]或者说第一个关键字是x[i-j],第二个关键字是x[i],其中i>j。
要先对第2关键字排序,再对第一个关键字排序,这就是基数排序的原理
12、13这两行
for(p=0,i=n-j;i
for(i=0;i
第14行中的
for(i=0;i
这里的wv[i]表示的就是第二个关键字排名第i位的k-前缀,它的第一个关键字的名次(相同字符串,就有相同的名次,如aa 都是0,ab都是1)。
第15-18行
for(i=0;i=0;i--) sa[--ws[wv[i]]]=y[i];
这四行中,与前面类似,是根据第一个关键的名次wv[i],对wv排序,将安装字典顺序排序好的2k-前缀放入sa中。
第19、20行
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i
至此,我们得到了后缀数组SA和名称数组Rank。
得到后缀数组并不是我们的目的,我们的目的是通过后缀数组来解决问题,这就需要另一个利器——最大公共前缀LCP。
定义1:对两个字符串 u,v 定义函数 lcp(u,v)=max{i|u=iv},也就是从头开始顺次比较u 和 v 的对应字符,对应字符持续相等的最大位置,称为这两个字符串的最长公共前缀。 我们所要找的实际上就是任意两个前缀suffix(sa[i])与suffix(sa[j])的LCP,用LCP(i,j)表示。
性质1:
LCP(i,j) = min{LCP(k-1,k) | i+1<=k<= j },称为 LCP Theorem。
所以只要知道了所以相邻的前缀的LCP,就可以求任意两个前缀的LCP了。
推论1:对于i < k-1 < k, LCP(i,k) <= LCP(k-1,k)
定义2:height[i] = LCP(i-1,i), 1 < i <= n,(在此处,i是后缀数组sa的下标) 。所以关键是求出数组height[]
定义3:h[i] = height[ Rank[i] ],也就是suffix[i]和他前一名的后缀的最大公共前缀(在此处,i是原始字符串r的下标)。
ps:height数组与h数组能够相互转化,height[i] = h[ sa[i] ]
性质2:
h[i] >= h[i-1] - 1
此性质可以根据推论1得到,证明如下:
而suffix[k] 排在suffix[i-1]前面,那么suffix[k+1]也一定排在suffix[i]前面,也就是Rank[k+1] < Rank[i]
根据推论1,有LCP(Rank[k+1],Rank[i]) <= LCP(Rank[i] - 1,Rank[i])
根据h[i]的定义,LCP(Rank[i] - 1,Rank[i])就是h[i]
因为 suffix[k] 与 suffix[i-1]的一个字符是相同的,所以去掉第一个字符后LCP(Rank[k+1],Rank[i]) = h[i-1] - 1
所以有,h[i] >= h[i-1] - 1 , 问题得证。
有了上述性质,我们可以令 i 从 1 循环到 n 按照如下方法依次算出 h[i]:
实现的时候其实没有必要保存 h 数组,只须按照h[1] ,h[2] ,… … ,h[n]的顺序计算即可。
下面是罗穗骞的论文中的代码:
int rank[maxn],height[maxn];
void calheight(int *r,int *sa,int n)
{
int i,j,k=0;
for(i=1;i<=n;i++) rank[sa[i]]=i;
for(i=0;i
for(i=1;i<=n;i++) rank[sa[i]]=i;
是根据sa数组的值来获取Rank数组,保证Rank数组中的排名是没有重复的。笔者认为这一步可以去掉,因为我们在前面得到的x[]数组就是排名数组,而所有后缀一定是不相同的,所以他们的排名数组不可能有重复的,可以直接在calheight函数的参数中传入此数组作为参数。
在第6、7行中
for(i=0;i
for(k?k--:k=0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
因为h[i]只是用到了h[i-1]的值,对于其他值没有用到,只须按照h[1] ,h[2] ,… … ,h[n]的顺序计算即可,不用保存h数组。h[i] = height[Rank[i]] = k,这里的k就是h[i]的值,通过第7行求得。在第7行中,k保存的是前一步的h数组值,也就是h[i-1],首先要先判断h[i-1] 是否大于等于 1,若是则从h[i-1] - 1位置开始,代码中的k--,若不是则从头开始,代码中的“0”,j就是排名在从i开始后缀前面的后缀的开始位置。对与从i,j开始的字符串suffix(i),suffix(j),他们的前k(此处k已经进行了修正)个字符都是相同,所以直接比较后面的字符,直到不相同位置,最后k就是最大公共前缀的长度。
自此,height数组就已经求得。
3、应用
例1:字符串S的最长重复子串
算法分析:
这道题目是后缀数组的一个简单应用。首先,最大重复子串等价于求两个后缀的最长公共前缀的最大值。任意两个后缀的公共前缀都是height数组中每一段的最小值,所以这个值一定不会大于height数组的最大值。所以最长重复子串的最大值就是height数组的最大值,从头到尾遍历一遍就可以求得。
例2:最大回文子串
算法分析:
对于字符串S
a b c d c b e f
我们在字符串最后加上#表示结束,同时加上它的反转字符串S‘,得到T:
a b c d c b e f # f e b c d c b a
那么对于中心位置d来说,在T中位置为i=3,它反转后得到的字符的位置为j = 2n - i + 1,其中n为字符串的长度。此时suffix(i) 与 suffix(2n-i+1)的最大公共前缀是最大的。根据这一原理,我们从i = 0 to n 来遍历,如果S[i]刚好是最长回文的中心位置,则suffix[i] 与 suffix[2n-i+1]的最大公共子串一定是最大值。所以问题也就变成了求任意两个后缀的最大公共前缀的问题,for i = [0,n) 求LCP(Rank[i],Rank[2n-i+1]),也就是MRQ问题。通过O(nlogn)的预处理,可以在O(1)时间内得到任意两个后缀的最大公共前缀。
例3:不相同子串个数
算法分析:
每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。如果所有的后缀按照suffix(sa[1]) , suffix(sa[2] ),suffix(sa[3]), …… ,suffix (sa[n] )顺序计算,不难发现,对于每一次新加进来的后缀suffix( sa[ k]) , 它将产生后缀长度个新的前缀,也就是n -sa[ k] + 1 。但是其中有height [k] 个是和前面的字符串的前缀是相同的。所以suffix (sa[k] ) 将“贡献 ” 出n-sa[k] +1 - height[ k ] 个不同的子串。累加后便是原问题的答案。这个做法的时间复杂度为 O(n) 。
最后附上源代码下载地址:http://download.csdn.net/detail/dylantsou/4543133
参考资料:
blog:http://zhan.renren.com/tag?value=%E7%99%BE%E5%BA%A6%E4%B9%8B%E6%98%9F
论文:后缀数组——处理字符串的有力工具 罗穗骞
IOI2004 国家集训队论文 许智磊