下面的例子取自刘汝佳大白书。
后缀数组是一个强力的字符串处理工具。
首先定义后缀i是以下标i为首到字符串末的子字符串。
而后缀数组sa定义为以字典顺序记录后缀。
所以,字符串AABAAAAB的后缀数组sa={3,4,5,0,6,1,7,2}。
仅仅只有后缀数组还不够,还需要构造一个名次数组ra。
名次数组ra定义为对应后缀的名次,上述字符串的名次数组ra={3,5,7,0,1,2,4,6}。
除此以外,还有一个height数组,height[i]定义为后缀sa[i-1]与后缀sa[i]的公共前缀。
上述字符串的height={0,3,2,3,1,2,0,1}。
文字描述可能比较难懂,通过图画就容易了。
图中还提供了如何使用height数组求两个后缀的最长公共前缀。即对height数组进行RMQ(ra[i]+1,ra[j])。
下面讲述如何求这三个数组。首先求sa数组。
使用倍增算法,使用基数排序。
首先对字母分配一个关键字,如AABAAAAB,则可以按照顺序为每个字母分配{1,1,2,1,1,1,1,2}。
然后对字母进行组合,把两个字符组合进行排序。则有AA,AB,BA,AA,AA,AA,AB,B#(#表示空字符,空字符分配为0)。
然后再对这些字符串分配关键字为{1,2,4,1,1,1,2,3},依次类推在两两组合,显然基数排序时间复杂度为O(n),总共logn个循环,总时间复杂度O(nlogn)。
ra数组的计算,可以证明ra[sa[i]]=i。
height数组的计算:
使用一个辅助数组h[i]=height[ra[i]]。该辅助数组有一个性质h[i]>=h[i-1]-1。
性质证明:设后缀i-1的前一个为后缀k,已经求出h[i-1]=height[ra[i-1]]。
分别删除后缀k和后缀i-1的首字符,得到后缀k+1和后缀i,显然h[i]>=h[i-1]-1。
所以可以使用一个循环计算height数组。
代码如下:
char ch[maxn];
int sa[maxn];
int ra[maxn];
int height[maxn];
int t1[maxn];//辅助数组
int t2[maxn];//辅助数组
int c[maxn];//辅助数组,基数排序的容器
void init(int m,int n)//m表示关键字的取值范围,n为字符串的长度
{
for(int i=n;i<n*2;i++)t1[i]=-1,t2[i]=-1;//模板用时发现问题,是数组用时没有初始化
int i,*x=t1,*y=t2;
//首先对一个字母的子串排序
for(i=0;i<m;i++)c[i]=0;//容器清零
for(i=0;i<n;i++)c[x[i]=ch[i]-'A']++;//计算对应关键字容器的元素个数
for(i=1;i<m;i++)c[i]+=c[i-1];//基数排序中计算容器下标
for(i=n-1;i>=0;i--)sa[--c[x[i]]]=i;//获得对应sa数组
for(int k=1;k<=n;k<<=1)
{
int p=0;
//使用辅助数组对第二关键字排序
for(i=n-k;i<n;i++)y[p++]=i;//k以后的后缀合并时必定与一个空字符串作为第二关键字
for(i=0;i<n;i++)if(sa[i]>=k)y[p++]=sa[i]-k;//按第二关键字顺序把第一关键字下标放入辅助数组
//最后y数组按第二关键字记录着第一关键字的下标
for(i=0;i<m;i++)c[i]=0;//容器清零
for(i=0;i<n;i++)c[x[i]]++;//计算对应关键字容器的元素个数
for(i=1;i<m;i++)c[i]+=c[i-1];//基数排序中计算容器下标
for(i=n-1;i>=0;i--)sa[--c[x[y[i]]]]=y[i];//计算sa数组
swap(x,y);//交换辅助数组
p=1;x[sa[0]]=0;
for(i=1;i<n;i++)
x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]?p-1:p++;//重新计算关键字,可以证明最后k个关键字一定不相同
if(p>=n)break;//当关键字数量大于等于n时结束排序
m=p;
}
//计算ra数组和height数组
for(i=0;i<n;i++)
ra[sa[i]]=i;
int k=0;
int j;
height[0]=0;
for(i=0;i<n;i++)
{
if(k)k--;
if(ra[i]-1<0){k=0;continue;}
j=sa[ra[i]-1];
while(i+k<n&&j+k<n&&ch[i+k]==ch[j+k])k++;
height[ra[i]]=k;
}
}
上述代码中,sa[--c[x[y[i]]]]=y[i]比较难理解。
首先y数组按顺序记录第一关键字下标,所以反过来使用则按逆顺序。
而x数组记录对应子串的关键字,则使用x[y[i]]可以时第二关键字大的靠后的下标。(x[y[i]]为第二关键字排序后的数组)