后缀数组相对后缀树来说比较容易构建,应用也比较广泛,和lcp,RMQ问题联合运用在各种竞赛中叶比较热门,究竟什么是后缀数组,怎么构建和应用,是像我这样的初学者遇到的最大的问题。
直观来说,后缀数组是记录一个字符串的后缀的排名的数组,什么是后缀呢,设一个字符串的长度是len(我们约定字符串下标从0开始,所以到len-1结束,比较符合我们日常编程习惯),某一位置i的后缀的就是从i开始到len-1结束的字符串,用suffix(i)表示,即对字符串s来说,suffix(i) = s[i,i+1....len-1],可以发现不同的后缀按字典序排列的名词是不一样的(什么是字典序都应该知道吧),记录这些后缀按字典序排好的结果的数组就叫后缀数组,一般用sa[]表示,网络上对sa[]的标准定义为:
后缀数组:后缀数组SA 是一个一维数组,它保存1..n 的某个排列SA[1],SA[2],……,SA[n],并且保证Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n。也就是将S 的n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入SA 中。
另外还要用到排名数组,即某一位置的后缀在所有后缀中的排名的数组,一般用Rank[]表示,容易发现Rank[sa[i]]=i。
名次数组:名次数组Rank[i]保存的是Suffix(i)在所有后缀中从小到大排列的“名次”。
简单的说,后缀数组是“排第几的是谁?”,名次数组是“你排第几?”。
知道了这些定义,剩下的就是如何构建后缀数组了,可以按照定义来构建,把每个后缀当做一个字符串,用全速排序来排序,不过那样的时间复杂度为O(n*n),一般用来构建后缀数组用的是倍增算法( Doubling Algorithm ),说到倍增算法,就要说到k-前缀的定义,字符串u的k-前缀就是u的从0开始到k-1的字串,u长度不足k时就是整个字符串u,这样一来我们在比较串s的两个位置i,j的后缀的2k-前缀时,就是比较两个后缀的k-前缀和两个后缀位置+k的k-前缀,显然当k=1时就是对整个串的单字符进行排序,复杂度O(nlogn),当k>=2时对已排好的k-前缀进行排序,用快排,复杂度O(nlogn),用基数排序,复杂度O(n),容易发现k是2倍增的。所以整个过程的时间复杂度就是O(nlongn)。倍增算法构建sa[]的代码如下:
#define max 10000 int Rx[max],Ry[max],rx[max]; int cmp(int *y,int a,int b,int l) { return y[a] == y[b] && y[a+l] + y[b+l];} //对于串约定最后一位是小于串中其他任何元素的元素,这样cmp的时候就不用担心y[a+l] //越界了,因为y[a] = y[b]就暗含了他们长度相等,都没有包含最后一位。 void get_sa(char *s,int *sa) { int len = strlen(s),*Rank_x = Rx,*Rank_y = Ry,bar[max],*result_x = rx; int i,j,k,p,*t,m=255; for (i = 0; i<= m; i++)//对字符排序不会超过255,根据实际情况m值自定 bar[i] = 0; for (i = 0; i< len; i++) bar[Rank_x[i] = s[i]]++; for (i = 1; i<= m; i++) bar[i] += bar[i-1]; for (i = len-1; i>= 0; i--) sa[--bar[Rank_x[i]]] = i; //这段代码用到桶排序思想,就是先进桶,再从不同桶里一个一个往外倒 //sa[]保存的是1-前缀排序结果,Rank_x[]保存的是1-前缀时的各位置的排名 for (k = 1,p = 1; p < len; k *= 2, m = p){ for (p = 0,i = len - k; i < len; i++) Rank_y[p++] = i; for (i = 0; i< len; i++) if (sa[i] >= k) Rank_y[p++] = sa[i] - k; //这段代码对1-前缀时做第二关键字排序 for (i = 0; i< len; i++) result_x[i] = Rank_x[Rank_y[i]]; for (i = 0; i<= m; i++) bar[i] = 0; for (i = 0; i< len; i++) bar[result_x[i]]++; for (i = 1; i<= m; i++) bar[i] += bar[i-1]; for (i = len-1; i>= 0; i--) sa[--bar[result_x[i]]] = Rank_y[i]; //又用到了一次桶排序,注意体会,是在对第二关键字排好序的序列上对 //第一关键字进行桶排序求得了新的sa[],result_x[]保存的是关于第二关键字 //排好序的序列的第一关键字排名,为桶排序做好准备 for (t = Rank_x,Rank_x = Rank_y,Rank_y = t,p = 1,Rank_x[sa[0]]= 0,i = 1; i < len; i++) Rank[sa[i]] = cmp(Rank_y,sa[i],sa[i-1],k)?p-1:p++; //求新的名次数组,可以发现名次可能一样,当名次各不一样时就是排序完成时。 } }好了,先写到这儿,至于后缀数组的运用,有时间再写,下午还要比赛呢.....