基本上一搜后缀数组网上的模板都是《后缀数组——处理字符串的有力工具》这一篇的注释,O(nlogn)的复杂度确实很强大,但对于初次接触(比如窝)的人来说理解起来也着实有些困难(比如窝就活活好了两天的光阴。。),看了那么多材料感觉《挑战程序设计》的后缀数组解释理解起来会相对容易很多,然而它的复杂度是O(nlog2n)的,主要区别是对子串排序的时候前者用了基数排序--O(n),而后者用了快排--O(nlogn),这就导致了最终的复杂度后者比前者多了一个O(logn)。
先整理下《挑战》上的程序,晚些再将O(nlogn)整理下来;
先附清爽版求SA(Suffix_Array)模板,主要思想当然仍是倍增法;
1 /*0(nlog(n)^2)*/ 2 #include <iostream> 3 #include <cstring> 4 #include <cstddef> 5 #include <cstdio> 6 #include <string> 7 #include <algorithm> 8 using namespace std; 9 const int MAXN = 10001; 10 int n,k; 11 int rank[MAXN+1],tmp[MAXN+1]; 12 13 bool comp_sa(int i, int j) 14 { 15 if(rank[i] != rank[j]) 16 return rank[i] < rank[j]; 17 int ri = i+k <= n? rank[i+k] : -1; 18 int rj = j+k <= n? rank[j+k] : -1; 19 return ri < rj; 20 } 21 22 void calc_sa(string &S, int *sa) //计算字符串S的后缀数组 23 { 24 n = S.size(); 25 //初始长度为1 26 for(int i = 0; i <= n; i++) 27 { 28 sa[i] = i; 29 rank[i] = i < n ? S[i] : -1; 30 } 31 32 for( k = 1; k <= n; k *= 2) 33 { 34 sort(sa,sa+n+1,comp_sa); //双关键字快排 35 36 //先在tmp中临时存储新计算的rank,再转存回rank中 37 tmp[sa[0]] = 0; 38 for(int i = 1; i <= n; i++) 39 { 40 tmp[sa[i]] = tmp[sa[i-1]] + (comp_sa(sa[i-1],sa[i]) ? 1: 0); 41 } 42 for(int i = 0; i <= n; i++) 43 { 44 rank[i] = tmp[i]; 45 } 46 } 47 } 48 49 int main() 50 { 51 string S = "abracadabra"; 52 int *sa = new int[S.size()+1]; 53 SuffixArrayMatch(S,sa,T); 54 delete [] sa; 55 sa = NULL; 56 }
当然初次接触的人看这代码必然不知这是什么鬼,建议可以多找几篇blog的代码注释(有很多大神的注释很细致很不错),加上手动模拟理解一下,多看几遍肯定会开窍的~;
定义(理解!):
后缀数组Suffix_Array:SA[]
将某个字符串的所有后缀按字典序排序后得到的数组;
SA[i] = k即表示排序后第i小的子串为S[k ... n](为好理解暂用字符串下标1~n);
而最终我们要达到的SA数组状态如下例所示(摘自罗神的PPT):
名次数组:Rank[]
保存后缀S[i ... n]在排序中的名次;
rank[i] = k即表示子串S[i ... n]在所有后缀的字典序排序中为第k小;
通过图1可以看出SA[1] = 4, Rank[4] = 1; 即SA数组与Rank数组为互逆关系;
后缀数组的计算
算法的基本思想 -- 倍增
什么叫倍增?
首先计算每个位置开始的长度为1的子串的顺序,利用该结果计算长度为2的子串的顺序,再利用长度为2的子串的顺序结果计算长度为4的子串的顺序 ...... 不断倍增,知道长度大于等于原字符串长度,就得到了后缀数组;
要计算长度为2的子串的顺序,只要排序两个字符组成的数对即可。
比如原字符串 aaba (下面各字母的下标代表该字母在原字符串的位置)
a1=a2=a4 < b3,那么a1a2一定小于a2b3
要求长度为2k的子串的顺序,只要知道长度为k的子串的顺序即可。
比如原字符串 aabac
a1a2 < a2b3 < a4c5< b3a4,那么 a1a2b3a4 一定小于 a2b3a4c5
记rankk(i)为S[i, k](从i开始的长度为k的子串)在所有排好序的长度为k的子串中是第几小的;
要计算长度为2k的子串的顺序,就只要对两个rank组成的数对进行排序即可。通过对rankk(i)与rankk(i+k)的数对和rankk(j)与rankk(j+k)的数对比较(双元素比较)来代替对S[i, 2k]和S[j, 2k]的直接比较。因为比较rankk(i)与rankk(j)就相当于比较S[i, k]与S[j, k],比较rankk(i+k)与rankk(j+k)就相当于比较S[i+k, k]与S[j+k, k]。
举个例子: abracadabra
初始化:SA[i] = i;
rank[i] = S[i]; //初始的时为对字符串中的单个字符排序,故可以直接将rank初始为字符的ASCII码,注意此时的rank并非实际意义上的排序,仅仅是相对排序,即S[i]>S[j]则rank[i] > rank[j]而已;
还有需要注意的一点是此处有一个处理字符串的小技巧是将SA[n] 定义为 -1;这样以后的rank值就可以从1开始排了。
k = 0,初始化,得到S[i, 1]的排序;
sa[0] : 0 rank[0] : 97
sa[1] : 1 rank[1] : 98
sa[2] : 2 rank[2] : 114
sa[3] : 3 rank[3] : 97
sa[4] : 4 rank[4] : 99
sa[5] : 5 rank[5] : 97
sa[6] : 6 rank[6] : 100
sa[7] : 7 rank[7] : 97
sa[8] : 8 rank[8] : 98
sa[9] : 9 rank[9] : 114
sa[10] : 10 rank[10] : 97
sa[11] : 11 rank[11] : -1
k = 1;得到S[i, 2]的排序;
sa[0] : 11 rank[11] : 0
sa[1] : 10 a rank[10] : 1
sa[2] : 0 ab rank[0] : 2
sa[3] : 7 ab rank[7] : 2
sa[4] : 3 ac rank[3] : 3
sa[5] : 5 ad rank[5] : 4
sa[6] : 1 br rank[1] : 5
sa[7] : 8 br rank[8] : 5
sa[8] : 4 ca rank[4] : 6
sa[9] : 6 da rank[6] : 7
sa[10] : 2 ra rank[2] : 8
sa[11] : 9 ra rank[9] : 8
k = 2;得到S[i, 4]的排序;
sa[0] : 11 rank[11] : 0
sa[1] : 10 a rank[10] : 1
sa[2] : 0 abra rank[0] : 2
sa[3] : 7 abra rank[7] : 2
sa[4] : 3 acad rank[3] : 3
sa[5] : 5 adab rank[5] : 4
sa[6] : 8 bra rank[8] : 5
sa[7] : 1 brac rank[1] : 6
sa[8] : 4 cada rank[4] : 7
sa[9] : 6 dabr rank[6] : 8
sa[10] : 9 ra rank[9] : 9
sa[11] : 2 raca rank[2] : 10
k = 4;得到S[i, 8]的排序;
sa[0] : 11 rank[11] : 0
sa[1] : 10 a rank[10] : 1
sa[2] : 7 abra rank[7] : 2
sa[3] : 0 abracada rank[0] : 3
sa[4] : 3 acadabra rank[3] : 4
sa[5] : 5 adabra rank[5] : 5
sa[6] : 8 bra rank[8] : 6
sa[7] : 1 bracadab rank[1] : 7
sa[8] : 4 cadabra rank[4] : 8
sa[9] : 6 dabra rank[6] : 9
sa[10] : 9 ra rank[9] : 10
sa[11] : 2 racadabr rank[2] : 11
k = 8;得到S[i, n]的排序;
sa[0] : 11 rank[11] : 0
sa[1] : 10 a rank[10] : 1
sa[2] : 7 abra rank[7] : 2
sa[3] : 0 abracadabra rank[0] : 3
sa[4] : 3 acadabra rank[3] : 4
sa[5] : 5 adabra rank[5] : 5
sa[6] : 8 bra rank[8] : 6
sa[7] : 1 bracadabra rank[1] : 7
sa[8] : 4 cadabra rank[4] : 8
sa[9] : 6 dabra rank[6] : 9
sa[10] : 9 ra rank[9] : 10
sa[11] : 2 racadabra rank[2] : 11
这样就得到了SA数组;
对于字符串的排序采用双关键字快排,复杂度为O(nlogn);每次计算后缀S[i ... n] 的 rank值时,先与该后缀排序后的前一个后缀比较,如果相等则rank值相同,否则rank值加1;
1 //先在tmp中临时存储新计算的rank,再转存回rank中 2 tmp[sa[0]] = 0; 3 for(int i = 1; i <= n; i++) 4 tmp[sa[i]] = tmp[sa[i-1]] + (comp_sa(sa[i-1],sa[i]) ? 1: 0); 5 6 for(int i = 0; i <= n; i++) 7 rank[i] = tmp[i];
晚些整理O(nlogn)算法