学渣乱搞系列之后缀数组
2014-08-23 11:23
295 查看
[b]学渣乱搞系列之后缀数组[/b]
by 狂徒归来
后缀数组,其nlogn的构造方法,比较麻烦,十几个循环,基数排序?计数排序?各种排序,各种凌乱,学渣表示鸭梨很大啊!学渣从《挑战程序设计竞赛》中偷学了一点nlog2n的构造方法。
字符串后缀(Suffix)是指从字符串的某个位置开始到其末尾的字符串子串。我们认为原串和空串也是后缀。
后缀数组(Suffix Array)指的是将某个字符的所有后缀按字典序排序后得到的数组。排序方式很多,时间复杂度也不同。有基数排序的倍增法o(nlogn),有DC3构造方法o(n),还有MF构造法等方法。今天我就学学最简洁但是时间复杂度稍高的构造方法,快排+倍增。o(nlog2n)的复杂度。代码量很少。
"abracadabra"对应的后缀数组
高度数组(LCP Array,Longest Commom Prefix Array)指的是由后缀数组中的相邻两个后缀的最长公共前缀的长度组成的数组。lcp[i]是后缀S[sa[i]...]与S[sa[i+1]...]的最长公共前缀。可以在o(n)的时间内求得lcp数组。
lcp的求取是有规律的。
lcp的求解过程
摘录书上原话:我们从位置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开始检查,计算最长公共前缀的长度就好了。
为了方便对拍,跟网上众多代码一样,不计入空串。所以我将书上代码稍微改动了一下。以后我的后缀数组模板就用这个了。速度是慢点。
本文内容取自《挑战程序设计竞赛》,只是因为写得很好,相对比较好理解,是我见过的,有关后缀数组最好理解的一篇文章,故将其记录到博客中。
2014/8/23
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 |
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <climits> #include <vector> #include <queue> #include <cstdlib> #include <string> #include <set> #include <stack> #define LL long long #define pii pair<int,int> #define INF 0x3f3f3f3f using namespace std; const int maxn = 100100; int n,k,_rank[maxn],tmp[maxn],sa[maxn],lcp[maxn]; string str; bool cmp_sa(int i,int j) { if(_rank[i] != _rank[j]) return _rank[i] < _rank[j]; 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) { for(int i = 0; i <= n; i++) { sa[i] = i; _rank[i] = i < n ? S[i]:-1; } for(k = 1; k <= n; k <<= 1) { sort(sa,sa+n+1,cmp_sa); tmp[sa[0]] = 0; for(int i = 1; i <= n; i++) tmp[sa[i]] = tmp[sa[i-1]] + cmp_sa(sa[i-1],sa[i]); for(int i = 0; i <= n; i++) _rank[i] = tmp[i]; } } void construct_lcp(string &S,int *sa,int *lcp) { for(int i = 0; i <= n; i++) _rank[sa[i]] = i; int h = 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; } } int main() { while(cin>>str) { n = str.length(); memset(sa,0,sizeof(sa)); memset(lcp,0,sizeof(lcp)); construct_sa(str,sa); construct_lcp(str,sa,lcp); } return 0; }
高度数组(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 |
为了方便对拍,跟网上众多代码一样,不计入空串。所以我将书上代码稍微改动了一下。以后我的后缀数组模板就用这个了。速度是慢点。
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <climits> #include <vector> #include <queue> #include <cstdlib> #include <string> #include <set> #include <stack> #define LL long long #define pii pair<int,int> #define INF 0x3f3f3f3f using namespace std; const int maxn = 10010; int n,k,_rank[maxn],sa[maxn],lcp[maxn],tmp[maxn]; bool cmp_sa(int i,int j) { if(_rank[i] != _rank[j]) return _rank[i] < _rank[j]; int ri = i + k < n ? _rank[i+k]:0; int rj = j + k < n ? _rank[j+k]:0; return ri < rj; } void construct_sa(char *s) { memset(sa,0,sizeof(sa)); for(int i = 0; i < n; i++) { sa[i] = i; _rank[i] = s[i]; } for(k = 1; k < n; k <<= 1) { sort(sa,sa+n,cmp_sa); tmp[sa[0]] = 0; for(int i = 1; i < n; i++) tmp[sa[i]] = tmp[sa[i-1]] + cmp_sa(sa[i-1],sa[i]); for(int i = 0; i < n; i++) _rank[i] = tmp[i]; } } void construct_lcp(char *s) { memset(lcp,0,sizeof(lcp)); for(int i = 0,h = 0; i < n; i++) { if(h) h--; for(int j = sa[_rank[i]+1]; i+h < n && j+h < n && s[i+h] == s[j+h]; h++); lcp[_rank[i]+1] = h; } } int main() { char str[] = "abracadabra"; n = strlen(str); construct_sa(str); construct_lcp(str); for(int i = 0; i < n; i++) cout<<i<<" "<<sa[i]<<endl; cout<<endl; for(int i = 0; i < n; i++) cout<<i<<" "<<lcp[i]<<endl; return 0; }
本文内容取自《挑战程序设计竞赛》,只是因为写得很好,相对比较好理解,是我见过的,有关后缀数组最好理解的一篇文章,故将其记录到博客中。
2014/8/23
相关文章推荐
- 学渣乱搞系列之网络流学习
- 后缀数组 (hihocoder重复旋律系列)
- 学渣乱搞系列之Tarjan模板合集
- 挑战程序竞赛系列(69):4.7后缀数组(1)
- 【后缀数组系列】二、后缀数组的两种求法
- 学渣乱搞系列之字符串滚动哈希
- 字符串系列4 后缀数组
- 后缀数组
- C++语言中数组指针和指针数组彻底分析(系列一)
- js入门系列演示·数组
- POJ 2758 后缀数组
- C/C++面试之算法系列--二维动态数组定义及二维静态数组与**P的区别
- 【我解C语言面试题系列】009 特殊的去除数组中重复数字问题
- 后缀数组(O(n)三分实现)
- js入门系列演示·数组
- 数据结构与算法(C#实现)系列---二叉堆(数组实现)
- 算法基础系列之五:数组排序类
- 【我解C语言面试题系列】004 数组的循环右移问题
- 【我解C语言面试题系列】008 去除数组中重复数字问题
- 【我解C语言面试题系列】012 查找整数数组中第二大的数