后缀数组
简述
后缀数组sa是一个一维数组,它是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入 SA 中。比如:BANANA的所有后缀是BANANA,ANANA,NANA,ANA,NA,A,按照字典序排列就是A,ANA,ANANA,BANANA,NA,NANA,那么后缀数组就是{5, 3, 1,0,4,2 }。
倍增算法
首先将单个字符排序,计算出每个字母的名词。
然后给所有后缀的前两个字符排序,这等价与给一些二元组排序,其中每个二元组就是一个后缀的前两个字符的名次。
接下来,是给每个后缀的前四个字符排序,注意,这次排序的对象仍然是二元组,注意到每个后缀k的前四个字符是由后缀k的前两个字符和后缀k+2的前两个字符组成,如果后缀k与k`相比,那么首先应该比较它们的前两个字符。
当所有名次已经两两不同,算法结束。
注意到字符种类最多m种,所以可以采用基数排序,算法每一轮的时间复杂度降为O(n),总的时间复杂度为O(nlogn)。
求解sa[]代码
void build_sa(int *r, int n, int m){
int i,j,p,*x = wa, *y = wb, *t;
for(i = 0; i < m; i ++) ws[i] = 0;//ws是基数排序中统计数量的辅助数组
for(i = 0; i < n; i ++) ws[x[i] = r[i]] ++; //统计各个桶中有多少个元素
for(i = 1; i < m; i ++) ws[i] += ws[i-1]; //ws[i]表示第i个桶右边的索引
for(i = n-1; i >= 0; i --) sa[--ws[x[i]]] = i; //每放一个元素ws[i]减少1个
for(j = 1, p = 1; p < n; j*=2, m = p){ //j代表当前字符串长度
for(p = 0, i = n-j; i < n; i ++) y[p++] = i;//后j个字符第二关键字是最小的
for(i = 0; i < n; i ++)
if(sa[i] >= j)
y[p++] = sa[i]-j;//y[]记录排好序的第二关键字的顺序
for(i = 0; i < n; i ++) wv[i] = x[y[i]];
for(i = 0; i < m; i ++) ws[i] = 0;
for(i = 0; i < n; i ++) ws[wv[i]] ++;
for(i = 1; i < m; i ++) ws[i] += ws[i-1];
for(i = n-1; i >= 0; i--) sa[--ws[wv[i]]] = y[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++; //如果两个后缀前j个字符相等名次与上一个后缀相同,否则排在其后,同时p可代表不同字符串的数量
//p >= n代表不需要继续倍增,每一轮,字符范围不会超过p,可将m更新为p
}
}
int cmp(int *r,int a,int b,int l) {
return r[a] == r[b] && r[a+l] == r[b+l];
}
求解height[]数组
只有sa数组可以做的事情并不多,我们还需要两个辅助数组rank[],height[]。rank[i]代表后缀i在sa数组中的下标。height[i]代表sa[i-1]和sa[i]的最长公共前缀长度。对于两个后缀j,k,不难证明j和k的LCP长度等于height[rank[j]+1],height[rank[j]+2],......,height[rank[k]]中的最小值,即RMQ(height, rank[j]+1, rank[k])。O(n)时间计算height需要一个辅助数组h[i] = height[rank[i]],然后递推。递推的关键是:h[i] >= h[i-1]-1。
void calheight(int *r, int n){
int i, j, k=0;
for(i=1; i<=n; i++)
rank[sa[i]] = i;
for(int i=0; i<n; i++){
if(k) k--;
int j = sa[rank[i]-1];
while(r[i+k] == r[j+k]) k++;
height[rank[i]] = k;
}
}