[知识点]后缀数组

// 本文部份内容参照刘汝佳《算法竞赛入门经典训练指南》,特此说明。html

 

[20190129更新!] 终于!时隔多年对这篇文章从新整理了一下,感谢你们提出的建议与意见。c++

 

一、前言算法

  趁着这几天上午,把后缀数组大体看完了。这个东西自己的概念可能没太大理解问题,可是它所延伸出来的知识很复杂,不少,还有它的两个兄弟——后缀树,后缀自动机,编起来都不是盖的。数组

 

二、概念优化

  前面曾经提到过AC自动机(http://www.cnblogs.com/jinkun113/p/4682853.html),讲得有点简略,它用以解决多模板匹配问题。可是前提是事先知道全部的模板,在实际应用中,咱们没法事先知道查询内容的,好比在搜索引擎中,你的查询是不可能直接预处理出来的。这个时候就须要预处理文本串而非每次的查询内容。搜索引擎

  后缀数组,说的简单一点,就是将一个字符串的全部后缀储存起来的数组,接下来分析它的做用。 spa

 

三、构建3d

  首先假定一个字符串BANANA,在后面添加一个非字母字符“$”,表明一个没出现过的标识字符,而后把它的全部后缀——code

  

  插入到一棵Trie中。因为标识字符的存在,字符串每个后缀都与一个叶节点一一对应。如图所示:htm

  咱们发现,有了后缀Trie以后,能够O(m)查找一个单词,如右侧。

  在实际应用中,会把后缀Trie中没有分支的链合并在一块儿,获得所谓的后缀树,可是因为后缀树的构造算法复杂难懂,且容易写错,因此在竞赛中不多使用,因此暂时不去研究了。相比之下,后缀数组是必备武器,时间效率高,代码简单,并且不易写错。

  在绘制后缀Trie的时候,咱们将字典序小的字母排在左边。因为叶节点和后缀一一对应,咱们如今在每个叶节点上标上该后缀的首字母在原字符串中的位置,如图:

  将全部下标连在一块儿,构建出来的,就是所谓的后缀数组了。BANANA的后缀数组为sa[] = {5, 3, 1, 0, 4, 2},举个例子,其中sa[1] = 3表示第3 + 1 = 4个字母开头的后缀即"ANA"在全部后缀中字典序排名为1。这样的话,咱们就能够直接经过一次快速排序O(n log n)获得了。可是,在比较任意两个后缀时,又须要O(n),故这是O(n^2 log n),根本扛不住。

 

四、倍增

  下面介绍Manber和Myers发明的倍增算法,时间复杂度O(n log n)(不采用基数排序的话就是O(n log^2 n))。

  首先对于全部单个字符排序(也能够理解成对于每个后缀的第1个字符排序,这样后面的步骤更易衔接),如图:

  对于每一个字母,咱们根据字典序给予其一个名次,则a->1,b->2,n->3。

  而接下来,咱们再给全部后缀的前两个字符排序(以前就是前一个),将相邻二元组合并,再次根据字典序给予一个名次,如图:

  而咱们如今获得了全部后缀的前2个字符的排名,注意这种方法是倍增思想,接下来要求的就是全部后缀的前4个字符的名次,由于可知对于后缀x的前4个字符是由后缀x的前2个字符和后缀x+2的前2个字符组成的,方法同上。如图:

  咱们也能够注意到,当咱们试图再去把全部后缀的前8个字符排一遍序的时候会发现,并无任何含义。首先,这个字符串的长度没有达到8,其次全部名词已经两两不一样,已经达到了咱们的目的。因此咱们能够分析出,这个过程的时间复杂度稳定为O(log n)。

  获得了序列a[]={4,3,6,2,5,1},a[i]表示后缀i的名次。然后咱们能够获得后缀数组了:sa[]={5,3,1,0,4,2}。(你要问我怎么获得的嘛?)

  我的认为,这个思路本身想一想仍是好些,仍是比较清晰的,起码我是先有思路再看懂网上文章的意思的。

  

五、基数排序

  比较的复杂度为O(log n),若是这个时候再用快速排序的话,依旧须要O(n log^2 n),虽然已经小多了!可是,这个时候若是使用基数排序,能够进一步优化,达到O(n log n)。

  首先先来介绍这个之前没听过的排序方法。设存在一序列{73,22,93,43,55,14,28,65,39,81},首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中,以下图左侧所示:

  获得序列{81,22,73,93,43,14,55,65,28,39}。再根据十位数排序,如右侧,将他们连起来,获得序列{14,22,28,39,43,55,65,73,81,93}。

  很好理解的一个排序。详细的内容不过多阐述。它的时间复杂度取决于数的多少以及数的位数。

  在构建后缀数组的过程当中,咱们能够发现最大位数为2(字母总共只有26个),用基数排序的复杂度明显小于快速排序。下面给出一个临时的后缀数组构建模板,能够发现不少地方的模板都长这个样子的。

 

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define MAXN 1005
 5 #define MAXM 30
 6 
 7 char ch[MAXN];
 8 int sa[MAXN], a[MAXN], t[MAXN], c[MAXN], n, m = MAXM, p;
 9 
10 int main() {
11 scanf("%s", ch), n = strlen(ch); 12 for (int i = 0; i < n; i++) c[a[i] = (ch[i] - 'a' + 1)]++; 13 for (int i = 1; i < m; i++) c[i] += c[i - 1]; 14 for (int i = n - 1; i >= 0; i--) 15 sa[--c[a[i]]] = i; 16 for (int k = 1; k <= n; k <<= 1) { 17 int p = 0; 18 for (int i = n - k; i < n; i++) t[p++] = i; 19 for (int i = 0; i < n; i++) if (sa[i] >= k) t[p++] = sa[i] - k; 20 for (int i = 0; i < m; i++) c[i] = 0; 21 for (int i = 0; i < n; i++) c[a[t[i]]]++; 22 for (int i = 0; i < m; i++) c[i] += c[i - 1]; 23 for (int i = n - 1; i >= 0; i--) sa[--c[a[t[i]]]] = t[i]; 24 swap(a, t); 25 p = 1, a[sa[0]] = 0; 26 for (int i = 1; i < n; i++) a[sa[i]] = (t[sa[i - 1]] == t[sa[i]] && t[sa[i - 1] + k] == t[sa[i] + k]) ? p - 1 : p++; 27 if (p >= n) break; 28 m = p; 29 } 30 return 0; 31 }

 

【对如上代码的注释】

n表示串的长度,m表示字符种类数。因为m没有直接给出,故初始赋值为30(大于可能出现的字符种类个数便可)。

 

六、最长公共前缀

  目前咱们获得的只有后缀数组一个东西。接下来就有一系列的延伸。好比说,在O(n log n)的时间内处理最长公共前缀,即LCP。求n个字符串LCP,暴力须要O(n^3),彻底不是一个级别。

  而利用后缀数组的话,一般须要两个数组,rank[i]表示后缀i在SA数组中的下标;height[i]表示sa[i-1]和sa[i]的最长公共前缀长度。对于两个前缀j和k,j<k,不妨设rank[j]<rank[k]。不可贵到,后缀j和k的LCP长度等于height[rank[j+x]](x∈[1,k-j])中的最小值,举一个例子就能明白。

  好仍是好理解的,可是想一想,根据定义,每次计算一对的height数组,都须要O(n),则共须要O(n^2),这显然让人感到不可忍,毕竟构建SA数组的时候都只须要O(n log n)。

  然而这个时候咱们再用个辅助数组a[i]=height[rank[i]],而后按照h[1],h[2]……h[n]的顺序递推计算。递推的关键在于这样一个性质:h[i]>=h[i-1]-1.这样就不须要从字符串开头计算了。以下方。

 

代码:

 

 1 int rank[MAXN], height[MAXN];
 2 
 3 void geth() {
 4   for (int i = 0; i < n; i++) rank[sa[i]] = i;
 5   for (int i = 0; i < n; i++) {
 6     if (k) k--;
 7     int j = sa[rank[i] - 1];
 8     while (ch[i + k] == ch[j + k]) k++;
 9     height[rank[i]] = k;
10   }
11 }

 

 

下面是该优化的证实:

  设排在后缀i-1前一个的是后缀k。后缀k和后缀i-1分别删除首字符以后获得后缀k+1和后缀i,所以后缀k+1必定排在后缀i的前面,而且最长公共点缀长度为h[i-1]-1,如图所示:

 

  这个h[i-1]-1是一系列h值的最小值,这些h值包括后缀i和排在它前一个的后缀p的LCP长度,即h[i]。所以h[i]>=h[i-1]-1。

 

七、总结

  这是一个很是高大上的东西,也许说这些看起来仍是易懂的,可是题目作起来仍是可以达到一种境界的。尤为还有后缀自动机等内容没有提。我认为后缀数组实际上是个很巧妙的东西,更况且加在上面的各类优化。

相关文章
相关标签/搜索