(一)后缀数组
-
后缀数组(Suffix Array)指的是将某个字符串的所有后缀按字典排序后得到的数组。不过数组中并不需要直接保存所有的后缀字符串,只要记录对应的起始位置就好了。我们用 S [ i . . . ] S[i...] S[i...]来表示字符串 S S S从位置 i i i开始的后缀。
"abracadabra"对应的后缀数组sa
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 |
- 后缀数组的计算
计算长度为 n n n的字符串 S S S的后缀数组。下面就给大家介绍其中较为简单的一种——由Manber和Myers发明的 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)复杂度的算法。
该算法的基本思想是倍增。首先计算从每个位置开始的长度为2的子串的顺序,再利用这个结果计算长度为4的子串的顺序,接下来计算长度为8的子串的顺序,不断倍增,直到长度大于等于 n n n就得到了后缀数组。长度为1的子串的顺序不需要计算,直接用字符的 A S C I I ASCII ASCII编码就可以得出顺序。
我们用 S [ i , k ] S[i,k] S[i,k]来表示从位置 i i i开始的长度为 k k k的字符串子串。其中,当从位置 i i i开始,剩余字符不足k个时,表示的是从位置 i i i开始到字符串末尾的子串。
计算abracadabra的后缀数组的过程
现在假设已经求得了长度为 k k k的子串的顺序,要求长度为 2 k 2k 2k的子串的顺序。记 r a n k k ( i ) rank_k(i) rankk(i)为 S [ i , k ] S[i,k] S[i,k]在所有排好序的长度为 k k k的字串中是第几小的。要计算长度为 2 k 2k 2k的子串的顺序,就只要对两个 r a n k rank rank组成的数对进行排序就好了。我们通过对 r a n k k ( i ) rank_k(i) rankk(i)与 r a n k k ( i + k ) rank_k(i+k) rankk(i+k)的数对和 r a n k k ( j ) rank_k(j) rankk(j)与 r a n k k ( j + k ) rank_k(j+k) rankk(j+k)的数对的比较来替代对 S [ i , 2 k ] S[i,2k] S[i,2k]和 S [ j , 2 k ] S[j,2k] S[j,2k]的直接比较。
int n, k;
int rk[MAX_N + 1];
int tmp[MAX_N + 1];
//比较(rank[i], rank[i + k]) 和 (rank[j], rank[j + k])
bool compare_sa(int i, int j)
{
if(rk[i] != rk[j])
return rk[i] < rk[j];
else
{
int ri = i + k <= n ? rk[i + k] : -1;
int rj = j + k <= n ? rk[j + k] : -1;
return ri < rj;
}
}
void construct_sa(string S, int *sa)
{
n = S.length();
for(int i = 0; i <= n; i++)
{
sa[i] = i;
rk[i] = i < n ? S[i] : -1;
}
//利用长度为k的排序结果对长度为2k的排序
for(int k = 1; k <= n; k *= 2)
{
sort(sa, sa + n + 1, compare_sa);
//先在tmp中临时存储新计算的rank,再转存回rank中
tmp[sa[0]] = 0;
for(int i = 1; i <= n; i++)
{
tmp[sa[i]] = tmp[sa[i - 1]] + (compare_sa(sa[i - 1], sa[i]) ? 1 : 0);
}
for(int i = 0; i <= n; i++)
{
rk[i] = tmp[i];
}
}
}
(二)高度数组
-
高度数组指的是后缀数组中相邻两个后缀的最长公共前缀(LCP, Longest Common Prefix)的长度组成的数组。记后缀数组为 s a sa sa,高度数组为 l c p lcp lcp,则有后缀 S [ s a [ i − 1 ] . . . ] S[sa[i - 1]...] S[sa[i−1]...]与 S [ s a [ i ] . . . ] S[sa[i]...] S[sa[i]...]的最长公共前缀的长度为 l c p [ i ] lcp[i] lcp[i]。记 r a n k [ i ] rank[i] rank[i]为位置 i i i开始的后缀在后缀数组中的顺序,即有 r a n k [ s a [ i ] ] = i rank[sa[i]] = i rank[sa[i]]=i。这里的 r a n k rank rank数组与前面的 r a n k rank rank数组其实是一样的。
-
我们从位置0的后缀开始,从前往后依次计算后缀 S [ i . . . ] S[i...] S[i...]与后缀 S [ s a [ r a n k [ i ] − 1 ] . . . ] S[sa[rank[i] - 1]...] S[sa[rank[i]−1]...](即后缀数组中的前一个后缀)的最长公共前缀的长度。
void construct_lcp(string S, int *sa, int *lcp)
{
int n = S.length();
int h = 0;
lcp[0] = 0;
for(int i = 0; i < n; i++)
{
//得到从位置i开始的后缀的前一个后缀在字符串的开始位置
int j = sa[rk[i] - 1];
if(h > 0)
h--;
for(; j + h < n && i + h < n; h++)
{
if(S[j + h] != S[i + h])
break;
}
lcp[rk[i]] = h;
}
}
以上内容来自《挑战程序设计竞赛》