一、后缀数组就是将字符串所有后缀排序后的数组,设字符串为S,令后缀Suffix(i)表示S[i..len(S)]。用两个数组记录所有后缀的排序结果:
Rank[i]记录Suffix(i)排序后的序号,即Suffix[i]在所有后缀中是第Rank[i]小的后缀
SA[i]记录第i位后缀的首字母位置,即Suffix[SA[i]]在所有后缀中是第i小的后缀
然后就是怎么快速求所有后缀的顺序了,其中的关键是如何减少两个后缀比较的复杂度
方法是倍增法,定义一个字符串的k-前缀为该字符串的前k个字符组成的串,关于在k-后缀上的定义Suffix(k,i)、SA[k,i]和Rank[k,i]类似于前,则有
若Rank[k,i]=Rank[k,j]且Rank[k,i+k]=Rank[k,j+k],则Suffix[2k,i]=Suffix[2k,j]
若Rank[k,i]=Rank[k,j]且Rank[k,i+k]<Rank[k,j+k],则Suffix[2k,i]<Suffix[2k,j]
若Rank[k,i]<Rank[k,j],则Suffix[2k,i]<Suffix[2k,j]
这样就能在常数时间内比较Suffix(2^k, i)之间的大小,从而对Suffix(2^k,i)时行排序,最后当2^k>n时,Suffix(2^k, i)之间的大小即为所有后缀之间的大小
于是求出了所有后缀的排序,有什么用呢?主要是用于求它们之间的最长公共前缀(Longest Common Prefix,LCP)
令LCP(i,j)为第i小的后缀和第j小的后缀(也就是Suffix(SA[i])和Suffix(SA[j]))的最长公共前缀的长度,则有如下两个性质:
对任意i<=k<=j,有LCP(i,j) = min(LCP(i,k),LCP(k,j))
LCP(i,j)=min(i<k<=j)(LCP(k-1,k))
第一个性质是显然的,它的意义在于可以用来证明第二个性质。第二个性质的意义在于提供了一个将LCP问题转换为RMQ问题的方法:
令height[i]=LCP(i-1,i),即height[i]代表第i小的后缀与第i-1小的后缀的LCP,则求LCP(i,j)就等于求height[i+1]~height[j]之间的RMQ,套用RMQ算法就可以了,复杂度是预处理O(nlogn),查询O(1)
然后height的求法要用到另一个数组:令h[i]=height[Rank[i]],即h[i]表示Suffix(i)的height值(同时height[i]就表示Suffix(SA[i])的height值),则有height[i]=h[SA[i]]
然后h[i]有个性质:
h[i] >= h[i-1]-1
用这个性质我们在计算h[i]的时候进行后缀比较时只需从第h[i-1]位起比较,从而总的比较的复杂度是O(n),也就是说h数组在O(n)的时间内解决了。求出了h数组,根据关系式height[i]=h[SA[i]]可以在O(n)时间内求出height数组,于是可以在O(n)时间内求出height数组,从而整个LCP问题就解决了^_^
然后后缀数组的应用就是利用它的LCP在需要字符串比较时降低复杂度。同时由于后缀数组的有序性可以很方便地使用二分
于是总结一下要点:
利用倍增算法在O(nlogn)的时间内对后缀数组进行排序
利用h数组的性质在O(n)的时间内求出储存排序后相邻后缀间的LCP数的组height
利用LCP的性质将平凡LCP问题转化为height数组上的RMQ问题
二、使用倍增算法(Prefix Doubling)构造后缀数组
如果采用对每个后缀排序的方法来生成后缀数组,即使采用快速排序,由于每次比较的对象是字符串(设输入字符串的长度为n),因此每一个比较操作的复杂度不 再是常数,而是O(n),快速排序本身的平均情况下时间复杂度为O(nlgn),因此总的时间复杂度是O(n^2*lgn),如果考虑到采用快速排序最坏情况下复杂度为O(n^2),那么最坏时总的复杂度为O(n^3),显然在输入串长度很大时这种做法不可取。
Prefix Doubling算法(倍增法)是构造后缀数组一个比较实用的算法。其基本思想是先计算出每个后缀的k-前缀的rank值,然后在此基础上计算每个后缀的 2k-前缀rank值,k从1开始。直到每个后缀都排出先后顺序为止(任何两个后缀都不会相等,换句话说,每个后缀终究都能排出先后顺序)。在处理2k-前缀时,只需要使用基数排序(radix sort)算法,先后对两位数字排序(可以采用计数排序算法(counting sort)对每一位数字排序)。在最坏情况下,需要做lgn次基数排序,每一次基数排序的操作次数为2*O(n),因此它的时间复杂度是O(nlgn)。 倍增法虽然没有达到像DC3算法的线性复杂度,但是它的优点是实现比较简单,因此在实践中常被采用。
在以下实现中,当k=1时,由于只需 要对输入串的每个字符排序,因此在采用基数排序时,只有一位数字需要排序。当k>1时,需要对两位数字排序(为考虑通用性,代码中 Tuple.digits数组长度可以取>=1的任何整数,而不限于两位数字的基数排序)。如果rank数组中某个后缀具有最大rank值,而且该 rank值等于输入串的长度,这时说明每一个后缀都排出了次序,因而可以提前终止程序。另外,假设字母表中最大的字符为MAX_CHAR。
三、应用:给你n个字符串,求出这n个字符串的最长公共子串。注意这里最长公共子串不是DP里面的LCS,这里必须要连续。
大致思路:
后缀数组的典型运用。首先把这些字符串相连在一起,中间用分隔符隔开,二分枚举公共子串长度。查看是否存在相邻的个后缀,他们分别属于n个字符串,且它们之间的最长公共前缀长度(height)大于枚举的长度
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int nMax = 200001;
int num[nMax];//把字符串连在一起,用数字表示
int sa[nMax], rank[nMax], height[nMax];
int wa[nMax], wb[nMax], wv[nMax], wd[nMax];
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 n, int m){ // 倍增算法 r为待匹配数组 n为总长度 m为字符范围
int i, j, p, *x = wa, *y = wb, *t;
for(i = 0; i < m; i ++) wd[i] = 0;
for(i = 0; i < n; i ++) wd[x[i]=r[i]] ++;
for(i = 1; i < m; i ++) wd[i] += wd[i-1];
for(i = n-1; i >= 0; i --) sa[-- wd[x[i]]] = i; //计数排序法计算SA
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]];//初始化k前缀字符串
for(i = 0; i < m; i ++) wd[i] = 0;
for(i = 0; i < n; i ++) wd[wv[i]] ++;
for(i = 1; i < m; i ++) wd[i] += wd[i-1];
for(i = n-1; i >= 0; i --) sa[-- wd[wv[i]]] = y[i];//k前缀字符串计数排序
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 ++;
}
}
}
void calHeight(int *r, int n){ // 求height数组。
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 -- : 0, j = sa[rank[i]-1]; r[i+k] == r[j+k]; k ++);
}
}
int loc[nMax],m;//loc[i]表示第i位属于哪个字符串
char str[nMax],res[nMax];
bool vis[1004];
bool check(int mid,int len){
int i,j,tot;
tot=0;
memset(vis,0,sizeof(vis));
for(i=2;i<=len;i++){
if(height[i]<mid){
memset(vis,0,sizeof(vis));
tot=0;
}
else{
if(!vis[loc[sa[i-1]]]){//第i-1个后缀数组所在的字符串是否被遍历
vis[loc[sa[i-1]]]=1;
tot++;
}
if(!vis[loc[sa[i]]]){
vis[loc[sa[i]]]=1;
tot++;
}
if(tot==m){
for(j=0;j<mid;j++){
res[j]=num[sa[i]+j]+'a'-1;
}res[mid]='\0';
return 1;
}
}
}
return 0;
}
int main(){
int n,k,i,j,a,b,sp,ans;
while(scanf("%d",&m)&&m){
sp=29; //分隔符
n=0;
ans=0;
for(i=1;i<=m;i++){
scanf("%s",str);
for(j=0;str[j];j++){
loc[n]=i;
num[n++]=str[j]-'a'+1;
}
loc[n]=sp;
num[n++]=sp++;
}
num[n]=0;
da(num, n + 1, sp);
calHeight(num,n);
int left=0,right=strlen(str),mid;//开始二分
while(right>=left){
mid=(right+left)/2;
if(check(mid,n)){ //判断长度为mid的串是否是所有字符串的公共子串
left=mid+1;
ans=mid;
}
else{
right=mid-1;
}
}
if(ans!=0){
printf("%s\n",res);
}
else{
printf("IDENTITY LOST\n");
}
}
return 0;
}