定义:给出字符串T,将T的所有后缀(包括空串和本身)排序后,得到一个顺序,定义sa[i]数组表示第i小的后缀是从T的哪个位置开始的,如:
T=aabcd
则有sa={0,1,2,3,4,5}
构建:直接模拟,得出所有的后缀快排,则O(n^2*log n)
效率不够,定义S[i,k]表示从第i个位置开始的长度为k的字符串,rank[i,k]表示在所有排好序的S[i,k]中的排名。
假设长度为k的后缀数组已经构建好了,那么我们现在构建2k的后缀数组:
1.定义compare_sa,让sa数组按此排序。
2.根据sa数组,更新rank数组
关键在于compare_sa,我们将S[i,2k]分解为S[i,k]和S[i+k,k],
已知S[i,k],S[i+k,k]的排名rank[i,k],rank[i+k,k],所以我们可以通过先比较rank[i,k]与rank[j,k]的大小,再比较rank[i+k,k],rank[j+k,k]的大小,从而确定S[i,2k]与S[j,2k]的大小关系,可以迅速的排序。
参考片段:
bool compare_sa(int i,int j){
if (rank[i] != rank[j]) return rank[i]<rank[j];
else{
int ri=i+k<=n?rank[i+k]:-1;
int rj=j+k<=n?rank[j+k]:-1;
return ri<rj;
}
}
void construct_sa(string S,int *sa){
n=S.length();
for (int i=0;i<=n;i++){
sa[i]=i;
rank[i]=i<n?S[i]:-1;
}
for (k=1;k<=n;k*=2){
sort(sa,sa+n+1,compare_sa);
tmp[sa[0]]=0;
for (int i=1;i<=n;i++)
tmp[sa[i]]=tmp[sa[i-1]]+(compare_sa(sa[i-1],sa[i])?1:0);
for (int i=0;i<=n;i++)rank[i]=tmp[i];
}
}
但仅仅有后缀数组,不足以解决所有问题,加上一个高度数组,将会更强大。
高度数组:
定义:lcp数组lcp[i]表示sa[i]与sa[i+1]所代表的后缀的最长公共前缀。
构建:设rank[i]表示位置i开始的后缀在sa中的顺序,则有rank[sa[i]]=i。
假设我们以求出S[i...]与S[k....]的公共前缀,则如果S[i+1....],S[k+1.....]的首字母没有发生改变,则lcp[i+1]=lcp[i]-1,对于个别发生改变的只需单独检索一下即可。
参考片段:
void construct_lcp(string S,int *sa,int *lcp){
int n=S.length();
for (int i=0;i<=n;i++)rank[sa[i]]=i;
int h=0;
lcp[0]=0;
for (int i=0;i<n;i++){
int j=sa[rank[i]-1];
if (h>0)h--;
for (;j+h<n && i+h<n;h++)
if (S[j+h] != S[i+h])break;
lcp[rank[i]-1]=h;
}
}
后缀数组在使用中非常灵活,如要求前缀等可以将字符串反转,求出发转后的后缀数组即为原字符串的前缀。
例:
1.最长公共子串:将S2接在S1后,则最长公共子串就在拼接后的子串的高度数组中。
2.字符串匹配:构造sa后,通过二分解决问题。(思维复杂度优于kmp,准确率高于哈希,在极端情况下效率更高)
3.最长回文:枚举中心,将原串与反转后的拼接,通过lcp求解。