学渣乱搞系列之后缀数组
by 狂徒归来
后缀数组,其nlogn的构造方法,比较麻烦,十几个循环,基数排序?计数排序?各种排序,各种凌乱,学渣表示鸭梨很大啊!学渣从《挑战程序设计竞赛》中偷学了一点nlog2n的构造方法。
字符串后缀(Suffix)是指从字符串的某个位置开始到其末尾的字符串子串。我们认为原串和空串也是后缀。
后缀数组(Suffix Array)指的是将某个字符的所有后缀按字典序排序后得到的数组。排序方式很多,时间复杂度也不同。有基数排序的倍增法o(nlogn),有DC3构造方法o(n),还有MF构造法等方法。今天我就学学最简洁但是时间复杂度稍高的构造方法,快排+倍增。o(nlog2n)的复杂度。代码量很少。
i | sa[i] | S[sa[i]...] |
0 | 11 | (空字符串) |
1 | 10 | a |
2 | 7 | abra |
3 | 0 | abracadabra |
4 | 3 | acadabra |
5 | 5 | adabra |
6 | 8 | bra |
7 | 1 | bracadabra |
8 | 4 | cadabra |
9 | 6 | dabra |
10 | 9 | ra |
11 | 2 | racadabra |
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <cmath> 5 #include <algorithm> 6 #include <climits> 7 #include <vector> 8 #include <queue> 9 #include <cstdlib> 10 #include <string> 11 #include <set> 12 #include <stack> 13 #define LL long long 14 #define pii pair<int,int> 15 #define INF 0x3f3f3f3f 16 using namespace std; 17 const int maxn = 100100; 18 int n,k,_rank[maxn],tmp[maxn],sa[maxn],lcp[maxn]; 19 string str; 20 bool cmp_sa(int i,int j) { 21 if(_rank[i] != _rank[j]) return _rank[i] < _rank[j]; 22 int ri = i+k <= n ? _rank[i+k]:-1; 23 int rj = j+k <= n ? _rank[j+k]:-1; 24 return ri < rj; 25 } 26 void construct_sa(string &S,int *sa) { 27 for(int i = 0; i <= n; i++) { 28 sa[i] = i; 29 _rank[i] = i < n ? S[i]:-1; 30 } 31 for(k = 1; k <= n; k <<= 1) { 32 sort(sa,sa+n+1,cmp_sa); 33 tmp[sa[0]] = 0; 34 for(int i = 1; i <= n; i++) 35 tmp[sa[i]] = tmp[sa[i-1]] + cmp_sa(sa[i-1],sa[i]); 36 for(int i = 0; i <= n; i++) _rank[i] = tmp[i]; 37 } 38 } 39 void construct_lcp(string &S,int *sa,int *lcp) { 40 for(int i = 0; i <= n; i++) _rank[sa[i]] = i; 41 int h = lcp[0] = 0; 42 for(int i = 0; i < n; i++) { 43 int j = sa[_rank[i]-1]; 44 if(h > 0) h--; 45 for(; j + h < n && i + h < n; h++) 46 if(S[j+h] != S[i+h]) break; 47 lcp[_rank[i]-1] = h; 48 } 49 } 50 int main() { 51 while(cin>>str) { 52 n = str.length(); 53 memset(sa,0,sizeof(sa)); 54 memset(lcp,0,sizeof(lcp)); 55 construct_sa(str,sa); 56 construct_lcp(str,sa,lcp); 57 } 58 return 0; 59 }
高度数组(LCP Array,Longest Commom Prefix Array)指的是由后缀数组中的相邻两个后缀的最长公共前缀的长度组成的数组。lcp[i]是后缀S[sa[i]...]与S[sa[i+1]...]的最长公共前缀。可以在o(n)的时间内求得lcp数组。
lcp的求取是有规律的。
i | _rank[i] | sa[_rank[i]-1] | lcp[_rank[i]-1] |
0 | 3 abracadabra | 7 abra | lcp[2] = 4 |
1 | 7 bracadabra | 8 bra | lcp[6] = 3 |
2 | 11 racadabra | 9 ra | lcp[10] = 2 |
3 | 4 acadabra | 0 abracadabra | lcp[3] = 1 |
4 | 8 cadabra | 1 bracadabra | lcp[7] = 0 |
5 | 5 adabra | 3 acadabra | lcp[4] = 1 |
6 | 9 dabra | 4 cadabra | lcp[8] = 0 |
7 | 2 abra | 10 a | lcp[1] = 1 |
8 | 6 bra | 5 adabra | lcp[5] = 0 |
9 | 10 ra | 6 dabra | lcp[9] = 0 |
10 | 1 a | 11 (空) | lcp[0] = 0 |
11 |
摘录书上原话:我们从位置0的后缀开始,从前往后一次计算后最S[i...]与后缀S[sa[rank[i]-1]...](即后缀数组中的前一个后缀)的最长公共前缀的长度。此时假设我们已经求得了位置i对应的高度hi,那么我们可以证明位置i+1对应的高度不小于hi-1.为什么呢?记k = sa[rank[i]-1],已知后缀S[i...]和S[k...]的头部hi个字符是相等的,那么后缀S[i+1...]和后缀S[k+1...]分别是二者去除首个字符的结果,所以它们头部hi-1个字符是相等的。虽然在后缀数组中,S[i+1...]前面一个元素未必就是S[k+1...],但即便如此,公共前缀的长度也是只增不减的。因此,只要从hi-1开始检查,计算最长公共前缀的长度就好了。
为了方便对拍,跟网上众多代码一样,不计入空串。所以我将书上代码稍微改动了一下。以后我的后缀数组模板就用这个了。速度是慢点。
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <cmath> 5 #include <algorithm> 6 #include <climits> 7 #include <vector> 8 #include <queue> 9 #include <cstdlib> 10 #include <string> 11 #include <set> 12 #include <stack> 13 #define LL long long 14 #define pii pair<int,int> 15 #define INF 0x3f3f3f3f 16 using namespace std; 17 const int maxn = 10010; 18 int n,k,_rank[maxn],sa[maxn],lcp[maxn],tmp[maxn]; 19 bool cmp_sa(int i,int j) { 20 if(_rank[i] != _rank[j]) return _rank[i] < _rank[j]; 21 int ri = i + k < n ? _rank[i+k]:0; 22 int rj = j + k < n ? _rank[j+k]:0; 23 return ri < rj; 24 } 25 void construct_sa(char *s) { 26 memset(sa,0,sizeof(sa)); 27 for(int i = 0; i < n; i++) { 28 sa[i] = i; 29 _rank[i] = s[i]; 30 } 31 for(k = 1; k < n; k <<= 1) { 32 sort(sa,sa+n,cmp_sa); 33 tmp[sa[0]] = 0; 34 for(int i = 1; i < n; i++) 35 tmp[sa[i]] = tmp[sa[i-1]] + cmp_sa(sa[i-1],sa[i]); 36 for(int i = 0; i < n; i++) 37 _rank[i] = tmp[i]; 38 } 39 } 40 void construct_lcp(char *s) { 41 memset(lcp,0,sizeof(lcp)); 42 for(int i = 0,h = 0; i < n; i++) { 43 if(h) h--; 44 for(int j = sa[_rank[i]+1]; i+h < n && j+h < n && s[i+h] == s[j+h]; h++); 45 lcp[_rank[i]+1] = h; 46 } 47 } 48 int main() { 49 char str[] = "abracadabra"; 50 n = strlen(str); 51 construct_sa(str); 52 construct_lcp(str); 53 for(int i = 0; i < n; i++) 54 cout<<i<<" "<<sa[i]<<endl; 55 cout<<endl; 56 for(int i = 0; i < n; i++) 57 cout<<i<<" "<<lcp[i]<<endl; 58 return 0; 59 }
本文内容取自《挑战程序设计竞赛》,只是因为写得很好,相对比较好理解,是我见过的,有关后缀数组最好理解的一篇文章,故将其记录到博客中。
2014/8/23