后缀数组(SA)

来源:互联网 发布:网络机柜全套 编辑:程序博客网 时间:2024/04/28 10:20

Preface

这其实并不是一个特别高深的算法,但是有的性质需要自己多推一下,慢慢理解

SA可以在ONlogN的复杂度内完成对一个字符串完成后缀的排序,并且可以在O(N)的复杂度内求出排名相邻的两个后缀的最长公共前缀(LCP),非常优秀的解决很多字符串问题。

进入正题

SA和Rank

如果把每个后缀求出来,用快排之类的排序排是需要ON2logN的复杂度的(因为还要一个个字符比较)
显然没有充分利用后缀之间的关系

要求出两个数组SA[]Rank[]SA[i]表示第i名的是谁,Rank[i]则是i开始的是谁

一般后缀排序有两种方法,倍增和DC3,这里只讲好打方便的倍增

我们设sf(i)表示第i个位置开头的后缀。

对于每个sf(i),定义它长度为k的前缀为sf(i,k)
显然有几个性质

  • sf(i,2k)<sf(j,2k)当且仅当sf(i,k)<sf(j,k)sf(i,k)=sf(j,k)并且sf(i+k,k)<sf(j+k,k)
  • sf(i,2k)=sf(j,2k)当且仅当sf(i,k)=sf(j,k)并且sf(i+k,k)=sf(j+k,k)

设数组SAk[],Rankk[]表示在长度为k的条件下的SA,Rank

那么完全可以利用Rank来比较大小

然而,在k长度前缀下可能有完全相同的字符串,那么他们的Rankk就是相等的

于是每个sf(i,2k),都有两个关键字Rankk(i),Rankk(i+k),可以利用双关键字基数排序O(N)求出SA2k[],Rank2k[],再到4k,8k,16k,最后当k大于等于N了,那么排序就结束了(显然超出部分并不影响)

当然,初始时你需要对每一个字符做一遍排序,相当于k=1
下面是构造的程序

#define fo(i,a,b) for(i=a;i<=b;i++)#define fod(i,a,b) for(i=a;i>=b;i--)void make(){    memset(ct,0,sizeof(ct));    int i,j,k,mx;    fo(i,1,n) ct[rank[i]=st[i]]++;//这里用的是计数排序,为了方便,直接用ASCII码来比大小    fo(i,1,m) ct[i]+=ct[i-1];    fod(i,n,1) SA[ct[rank[i]]--]=i;//求出SA1    mx=m;    for(j=1,k=0;k<n;j*=2,mx=k)//这里mx表示最大的Rank值,也就是不同的sf(i,k)数目(想想为什么),如果每一个字符串都不同,再排也就没有意义了,直接退出。    {        int p=0;        fo(i,n-j+1,n) s2[++p]=i;        fo(i,1,n) if(SA[i]>j) s2[++p]=SA[i]-j;//第二关键字直接用上一次的SA计算,s2表示按照第二关键字排序的sf(i,2k)        memset(ct,0,sizeof(ct));        fo(i,1,n) ct[rank[s2[i]]]++;        fo(i,1,mx) ct[i]+=ct[i-1];        fod(i,n,1) SA[ct[rank[s2[i]]]--]=s2[i];//这里必须用downto,因为要把s2排在后面的放的尽量靠后(所以要--)        r1[SA[1]]=k=1;        fo(i,2,n) r1[SA[i]]=(rank[SA[i-1]]==rank[SA[i]]&&rank[SA[i-1]+j]==rank[SA[i]+j])?k:++k;//求新一轮的Rank,k代表当前到第几名,也就是不同的rank个数        fo(i,1,n) rank[i]=r1[i];    }}

至此,O(NlogN)复杂度内求出SARank

最长公共前缀(LCP)

然而,往往光有SARank是并不够的,我们还需要LCP

lcp(i,j)表示sf(i)sf(j)的最长公共前缀
LCP(i,j)表示lcp(SA[i],SA[j])

height[i]表示LCP(i1,i),且令height[1]=0

于是有一些非常好的性质

  • LCP(i,j)=min(LCP(i,k),LCP(k,j)),i<kj(LCP定理)

证法较为复杂,见

http://baike.baidu.com/link?url=nYOXOZ0cAcRFHtSczK6JI05hTsSRlQTiKyvPds90yB7CjooOQfeNI3W1nVk04mNd8-lr07ehEnZICh8R_LgOg_

于是可得出一个推论(因为取了min)

  • LCP(i,j)LCP(i,k),ij<k

用文字说明,就是排名更靠近的后缀的最长公共前缀更长(我感觉是比较显然的)

有了这两个东西,我们就可以用RMQ之类的算法快速求出任意两个后缀的最长公共前缀了

关键在于,如何快速的求出height?

我们设h[i]=height[Rank[i]],即为lcp(i,SA[Rank[i]1]),也就是i位置开始的后缀和它的前一名的最长公共前缀。显然height[SA[i]]=h[i]

有一个非常非常重要的性质

  • i>1Rank[i]>1,则有h[i]h[i1]1

因为百科上证明不容易理解,我讲一个容易理解的版本

如果有i,j<N,lcp(i,j)1

那么

  • sf(i)<sf(j)sf(i+1)<sf(j+1)
  • lcp(i+1,j+1)=lcp(i,j)1

第二条看似难以理解,实际上非常简单,由于lcp(i,j)1,那么sf(i)sf(j)的首字符一定是相同的,那么sf(i+1),sf(j+1)相当于去掉首字符的sf(i)sf(j)

那么它也就显然成立了
有了这两条,我们来证明上面那个性质

h[i1]1,显然成立,因为h[i]0h[i1]1
h[i1]>1,即height[Rank[i]]>1,因为height[1]=0

j=SA[Rank[i]1](即i的前一名),k=SA[Rank[i1]1](即i-1的前一名)
h[i]=lcp(i,j),h[i1]=lcp(i1,k)

根据上面的第二条
h[i1]=lcp(i1,k)=lcp(i,k+1)+1,也就是lcp(i,k+1)=h[i1]1

现在我们只需要证明lcp(i,j)lcp(i,k+1)

想起了之前的推论了吗?

因为ji只差了1名,并且ki1(因为ki1的前一名)

所以k+1一定不等于i
那么ki至少差了一名

于是上面的性质得证

有了这个性质,我们就可以有O(N)的方法求出height

对于每一个h[i],我们直接从h[i1]1的长度开始暴力判断lcp(sf(i),sf(SA[Rank[i]1]))

求出h后再根据heighth的关系式求出height

每次判断看似是O(N)的,但是平摊到每一个就没有这么多了,最多判断3N个字符是否相等

下面是代码,真的非常短

void findh(){    height[1]=0;    int i=0,j;    fo(i,1,n)    {        if (rank[i]==1) continue;        j=max(height[rank[i-1]]-1,0);        while (st[i+j]==st[SA[rank[i]-1]+j]) j++;        height[rank[i]]=j;    }}

至此,大功告成
附上SA模版

http://blog.csdn.net/hzj1054689699/article/details/51398396

这里有一道SA的应用题,大家可以做一做
JZOJ[1598]文件修复,下面附上链接的题意和讲解

http://blog.csdn.net/hzj1054689699/article/details/51398299

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 小孩数学成绩差怎么办 理科生语文不好怎么办 小学阅读题不好怎么办 如果孩子考不好怎么办 6岁不认识数字怎么办 数学一点都不会怎么办 初一数学太差怎么办 三年级孩子数学差怎么办 三年级孩子数学很差怎么办 初中学习不好高中怎么办 四年级孩子数学不好怎么办 孩子学习不开窍怎么办 孩子学习太笨怎么办 老师是个小人怎么办 孩子写字太差怎么办 孩子写字下手重怎么办 孩子一年级数学不好怎么办 孩子数学理解能力差怎么办 智商情商都低怎么办 一年级孩子数学很差怎么办 一年级孩子数学差怎么办 一年级数学学不好怎么办 孩子成绩差该怎么办 小学生数学太差怎么办 小学数学基础差怎么办 孩子一年级学习不好怎么办 快两岁的宝宝老尿裤怎么办 戒母乳宝宝哭闹怎么办 三周岁不肯说话怎么办 两岁宝宝打人怎么办 刚开始跳绳腿疼怎么办 两周岁宝宝拉肚子怎么办 宝宝睡觉认人怎么办 宝宝脸不光滑怎么办 两周岁宝宝打人怎么办 分手后想念前任怎么办 孩子不学习该怎么办 小孩吃了牙膏怎么办 小孩子吃了牙膏怎么办 一岁宝宝龋齿怎么办 宝宝吃牙膏了怎么办