后缀数组

后缀数组

作者:dylantsou

出处:http://blog.csdn.net/dylantsou



引言:       

       首先看一个问题,查找一个字符串中的最大回文子串。例如:S ADAMSMADY,它的最大回文子串就是DAMSMAD。

这道题目的解法如下,先构造它的反转字符串S‘ YDAMSMADA,那么求回文问题也就转换成了求字符串S与S’的最大公共子串问题,这个题目可以通过列出S与S'的所有后缀,在他们中找最长公共前缀的方法来解决。可以用后缀树来实现,也可以通过后缀数组+LCS来实现,后者原理简单其实起来比较容易,本文重点阐述其方法。


1、原理

       倍增算法的核心就是利用k-前缀 Rankk来构造2k-前缀 Rank2k,在Rankk已经构造好的情况下,设u、v分别为从i、j位置开始的后缀,要比较i、j的2k-前缀,实际可以先比较k-前缀的suffix[i]与suffix[j],再比较k-前缀的suffix[i+k]与suffix[j+k],而suffix[i]与suffix[j]比较可以通过比较Rankk[i] 与Rankk[j]获得,Rankk[i] 与Rankk[i+k] 便是下文中的第一个关键字,与第二个关键字。

2、 倍增算法实现


2.1 获得SA 与 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;
}



通过这张图片,说明了程序运行的过程。


在第6-9行中

     	 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];	//ws[]中存放的是累计次数
     	 for(i=n-1;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<n;j*=2,m=p)中,j 代表步长,第一个和第二个关键字中间隔了j个字符       

       

       第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<n;i++) y[p++]=i;

              for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;

       实现了对第二个关键的排序。sa数组中是按照k-前缀的字典顺序排列的,要对第二个关键字排序,只需要对这个数组做简单的调整即可,用y数组表示调整后的结果。因为n-j后面的关键字,i+j>n,所以他们没有第二个关键字,所以统一把他们放在前面,这样,就已经按照第二个关键字有序了。但是,y数组中存放的是第一个关键字的开始位置,也就是suffix[i]中的i,所以y[p++] = sa[i] - j,此处的-j就是为了获得第一个关键字的开始位置。

第14行中的 

              for(i=0;i<n;i++) wv[i]=x[y[i]];

       这里的wv[i]表示的就是第二个关键字排名第i位的k-前缀,它的第一个关键字的名次(相同字符串,就有相同的名次,如aa 都是0,ab都是1)。

第15-18行

            	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];

这四行中,与前面类似,是根据第一个关键的名次wv[i],对wv排序,将安装字典顺序排序好的2k-前缀放入sa中。

第19、20行

            	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++;

在for循环的初始化中,将原来的x[]数组,用y[]数组来保存,在第20行中构建新的x[]数组。在sa数组中,suffix(sa[i-1]) <=suffix(sa[i]),若是等于则x[i] = x[i-1];若是不等于,则x[i] = x[i-1]+1。用p来表示不相同的前缀的个数。k-前缀不相同的后缀,一定是不相同的后缀,所以k-前缀中不相同的后缀数目一定<=n,当k >= n时,k-前缀中不相同的数目p等于n,此时结束循环。


至此,我们得到了后缀数组SA和名称数组Rank。


2.2 求取 Height & LCP

        得到后缀数组并不是我们的目的,我们的目的是通过后缀数组来解决问题,这就需要另一个利器——最大公共前缀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)

定义2height[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得到,证明如下:

  1. 对于h[i-1] < 1的情况,h[i] >= 0 > h[i-1] - 1,显然成立
  2. 对于h[i-1] >= 1 的情况,设suffix(k) 为排在suffix[i-1]前一名的后缀,则他们的最长公共前缀就是h[i-1]
            因为h[i-1] >= 1,说明suffix[k] 与 suffix[i-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]:

  • 若 Rank[i]=1,则 h[i]=0。字符比较次数为 0。
  • 若 i=1 或者 h[i-1]≤1,则直接将 Suffix(i)和 Suffix(Rank[i]-1)从第一个字符开始依次比较直到有字符不相同,由此计算出 h[i]。
  • 否则,说明 i>1,Rank[i]>1,h[i-1]>1,根据性质 3,Suffix(i)和 Suffix(Rank[i]-1)至少有前 h[i-1]-1 个字符是相同的,于是字符比较可以从 h[i-1]开始,直到某个字符不相同,由此计算出 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<n;height[rank[i++]]=k)
        for(k?k--:k=0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
    return;
}

在第5行中

                for(i=1;i<=n;i++) rank[sa[i]]=i;

        是根据sa数组的值来获取Rank数组,保证Rank数组中的排名是没有重复的。笔者认为这一步可以去掉,因为我们在前面得到的x[]数组就是排名数组,而所有后缀一定是不相同的,所以他们的排名数组不可能有重复的,可以直接在calheight函数的参数中传入此数组作为参数。

在第6、7行中

           for(i=0;i<n;height[rank[i++]]=k)

                  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

abcdcbe

         我们在字符串最后加上#表示结束,同时加上它的反转字符串S‘,得到T:

abcdcbe#ebcdcba

      

         那么对于中心位置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 国家集训队论文  许智磊



          

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值