后缀数组详解

后缀数组

后缀数组是一种处理字符串强有力的工具,但一开始看它求sa和rank,的预处理的时候,感到难以理解。但仔细读的话,还是对求后缀数组的过程还是很清晰的.
首先是rank和sa的概念.rank,rank数组保存的是从这个字符串这一位之后的后缀在这个字符串n(n为字符串的长度)个后缀中按字典序下来的排名,即使“你排第几?”,而sa则是相对于rank的一个逆数组,概念反过来了,则是“排名排第i位的是谁”。
怎么快速的求sa呢(求rank只需要把求好的sa逆过来即可),我们这里用到的是倍增算法,另有一种dc3算法暂不提及,详情参考罗穗骞的论文.倍增算法用一个函数来求解,总代码如下(等下详解):
void da(int *r,int *sa,int n,int m)//r 为字符串,sa为后缀数组,n是字符串长度,m是排名序号的范围.

{
   int j,p,* x=wa,* y=wb,*t;
   for(register int i=0;i<m;i++) ws[i]=0;
   for(register int i=0;i<n;i++) ws[x[i]=r[i]]++;//字符串保存从0开始,长度为n
   for(register int i=1;i<m;i++) ws[i]+=ws[i-1];
   for(register int i=n-1;i>=0;i--) sa[--ws[x[i]]]=i; //可以自己拿小数据模拟一下理解排序过程
   //以上是对长度为1的排序(第一关键字)
   for(j=1,p=1;p<n;j=j*2,m=p) //接下来是倍增 
   {
     for(register int i=n-j,p=0;i<n;i++) y[p++]=i;
     for(register int i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;
     //以上是对第二关键字的排序
     for(register int i=0;i<n;i++) wv[i]=x[y[i]];
     for(register int i=0;i<m;i++)  ws[i]=0;
     for(register int i=0;i<n;i++) ws[wv[i]]++;
     for(register int i=1;i<m;i++) ws[i]+=ws[i-1];
     for(register int i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];
     int 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;
}

现在我们来对这一长串代码进行分析。
首先我们对倍增之前的四行代码进行分析。这四行实际是先对字符后缀串长为1的sa的处理(即此时sa的保存是每一个字符的排名————为什么是每一个字符呢,因为说了此时字符后缀串长为1,即就是这个字符自己)。我们这里要对基数排序进行一下分析,了解一下过程,可以看到:

 for(register int i=0;i<n;i++) ws[x[i]=r[i]]++;//字符串保存从0开始,长度为n
   for(register int i=1;i<m;i++) ws[i]+=ws[i-1];

这两行是什么意思呢?我们首先需要把r数组里面保存的字符转化为整数类型的值(可以直接等于这个字符的ASCLL值),我们知道a的值小于b的值,那么如果一串字符aaaabbbaa,在上面代码的第一行运行后ws[1]=6,ws[2]=3,在运行完第二行的时候ws[1]=6,ws[2]=9,为什么ws[2]变大了呢?因为在字典序里面b是比a小的,在这里b值为2,a值为1,那么虽然b只有三个,排名也应乖乖地排在a后面(注意ws是一个值域桶类似的东西).后面我们便可以求出sa.(此时的sa还不是真正的sa,因为并不是后缀的sa,此时的sa只是每个字符后缀长度只为1的sa).
后面开始倍增。怎么倍增呢,我们知道按字典序比较是一位一位的比较,这样显然慢.我们利用倍增的思想,我们试着把一串后缀分成两部分,一部分一部分的比较.我们先把长度为1的sa算出来,再利用它把后缀长度为2的算出来,再利用刚算好的后缀长度为2的sa把长度为4的算出来(2+2=4,注意我们是分成了两部分,一部分一部分的比较,而不是一位一位的),直到倍增到每个字符后缀长度囊括了应有的长度(即从自己开始所有以后的,真正的后缀),sa也就是真正的sa了.
现在对具体代码进行分析.
倍增开始的第一行j就是倍增的长度,m仍是范围(当前排名的,当排名末尾已经是n-1了(从0开始排)时,就说明真正的sa已经出炉了).
我们来看一看循环中的代码,

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

这里是对第二关键字的排名,此时第二关键字就是之前所说的第二部分,显然第二关键字可以由之前的sa得出来。这里代码的第一行是什么意思呢?为什么从n-j开始?因为此时这里从n-j开始的第二关键字没有。为什么么没有?因为此时第一部分长度为j,那么n-i+j>=n,而我们最高是n-1(从0开始的,长度为n),那没就没有第二关键字,显然a是比aa大的(在字典序里),所以没有第二关键字的排名当然靠前,y此时是第二关键字的排名.第二行就是直接的利用上次的sa来计算.

for(register int i=0;i<n;i++) wv[i]=x[y[i]];
     for(register int i=0;i<m;i++)  ws[i]=0;
     for(register int i=0;i<n;i++) ws[wv[i]]++;
     for(register int i=1;i<m;i++) ws[i]+=ws[i-1];
     for(register int i=n-1;i>=0;i--) sa[--ws[x[i]]]=y[i];

这里是综合第二关键字及第一关键字的sa,还是之前的原理,但是这里是按第一关键字为桶,是因为第一关键字比第二关键字更重要(ac>ca),就是ws中已经遵守了按第一关键字的排序规则了,因为我们看到 for(register int i=1;i< m;i++) ws[i]+=ws[i-1],值为i的排名一定是排在值为i-1后面的,这就巧妙地符合了规则,同时再利用y[i]里第二关键字就能得到新的sa。
最后要算出新的x.

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

算出新的x是为了方便下一遍比较关键字的值,就好比我数学考了130分,你也考了130,我们俩从分值来看排名是一样的,这就是x数组.而sa数组是我考了130,你也考了130,但可能因为姓名字母的问题你排的比我次一名——当然,最后的sa(真正的sa)肯定不会出现这种情况,因为后缀长度都不一样,即不可能会存在值相同的情况,这个排名是绝对公平公正的.
比如aabaaaab的例子,x一开始值是11211112,但是这里后缀长度为1后(第一遍循环后),有了第二关键字的插入,应该改为12411123,比如此时第4名在第三位,就是ba;第三名在最后一位,是b(第二关键字无);第2名在第一位(6),是ab;第一名在0(3,4,5)位置上;这一切符合字典序(在后缀长度为1的情况下),后来不断循环滚动倍增就好啦!


Tips:
1.y数组存的是以第二关键字排名的,第一关键字的位置.
2.以第二关键字排名的威力是显现在桶倒着来取的时候,因为此时排名越靠后的只会拿到更大值及排名更靠后的桶.
3.因为桶在不断–,所以也就保证了sa没有重复排名的情况.
4.注意p-1,而不是p++

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值