【经典数据结构】后缀数组

  转自:http://www.acmerblog.com/suffix-array-6150.html

  在字符串处理当中,后缀树和后缀数组都是非常有力的工具,其中后缀树大家了解得比较多,关于后缀数组则很少见于国内的资料。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。

  后缀树组是一个字符串的所有后缀的排序数组。后缀是指从某个位置 i 开始到整个串末尾结束的一个子串。字符串 r 的从 第 i 个字符开始的后缀表示为 Suffix(i) ,也就是Suffix(i)=r[i..len(r)] 。

  例子:

 1 字符串: "banana"的所有后缀如下:
 2
 3 0 banana                          5 a
 4 1 anana     对所有后缀排序        3 ana
 5 2 nana      ---------------->     1 anana
 6 3 ana        字典序               0 banana
 7 4 na                              4 na
 8 5 a                               2 nana
 9
10 所以 "banana" 的后缀数组SA为: {5, 3, 1, 0, 4, 2}

名次数组:名次数组Rank[i]保存的是以i开头的后缀的排名,与SA互为逆。简单的说,后缀数组是“排在第几的是谁”,名次数组是“你排第几”。

构造算法

  求解后缀数组的算法主要有两种:倍增算法DC3算法。在这里使用的是许智磊的倍增算法,复杂度为nlogn。

  关于详细求解后缀数组的算法,详见许智磊2004国家集训队论文

  这里只给出最直接的求解算法,就是先求得所有的后缀子串,再进行一次排序。

  

 1 // 朴素的后缀树组构造算法
 2 #include <iostream>
 3 #include <cstring>
 4 #include <algorithm>
 5 using namespace std;
 6
 7 // 表示一个后缀,index是后缀的开始下标位置
 8 struct suffix
 9 {
10     int index;
11     char *suff;
12 };
13
14 // 字典序比较后缀
15 int cmp(struct suffix a, struct suffix b)
16 {
17     return strcmp(a.suff, b.suff) < 0? 1 : 0;
18 }
19
20 // 构造txt的后缀数组
21 int *buildSuffixArray(char *txt, int n)
22 {
23     //结果
24     struct suffix suffixes[n];
25
26     for (int i = 0; i < n; i++)
27     {
28         suffixes[i].index = i;
29         suffixes[i].suff = (txt+i);
30     }
31
32     // 排序
33     sort(suffixes, suffixes+n, cmp);
34
35     // 排在第几的是谁
36     int *suffixArr = new int[n];
37     for (int i = 0; i < n; i++)
38         suffixArr[i] = suffixes[i].index;
39
40     return  suffixArr;
41 }
42
43 //打印
44 void printArr(int arr[], int n)
45 {
46     for(int i = 0; i < n; i++)
47         cout << arr[i] << " ";
48     cout << endl;
49 }
50
51 int main()
52 {
53     char txt[] = "banana";
54     int n = strlen(txt);
55     int *suffixArr = buildSuffixArray(txt,  n);
56     cout << "Following is suffix array for " << txt << endl;
57     printArr(suffixArr, n);
58     return 0;
59 }

输出:

  

1 Following is suffix array for banana
2 5 3 1 0 4 2

如何利用后缀数组来匹配字符串?

在回到那个经典的字符串匹配问题,如何在text中查找模式串pattern?有了后缀数组,我们就可以用二分查找来进行搜索。下面是具体的算法:

  

 1 void search(char *pat, char *txt, int *suffArr, int n)
 2 {
 3     int m = strlen(pat);
 4
 5     int l = 0, r = n-1;
 6     while (l <= r)
 7     {
 8         // 查看 ‘pat‘是否是中间的那个后缀的前缀字串
 9         int mid = l + (r - l)/2;
10         int res = strncmp(pat, txt+suffArr[mid], m);
11
12         if (res == 0)
13         {
14             cout << "Pattern found at index " << suffArr[mid];
15             return;
16         }
17         if (res < 0) r = mid - 1;
18         else l = mid + 1;
19     }
20     cout << "Pattern not found";
21 }
22
23 int main()
24 {
25     char txt[] = "banana";  // text
26     char pat[] = "nan";   // 模式串
27
28     // 构造后缀数组
29     int n = strlen(txt);
30     int *suffArr = buildSuffixArray(txt, n);
31
32     // 在txt中搜索pat是否出现
33     search(pat, txt, suffArr, n);
34     return 0;
35 }

  上面这个搜索算法的复杂度为O(mLogn),其实还有更高效的基本后缀数组的算法,后续再做讨论。

后缀数组的应用

先定义height数组,height[i] = suffix(SA[i-1])和suffix(SA[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。

  例1:最长公共前缀
  给定一个串,求任意两个后缀的最长公共前缀。
  解:先根据rank确定这两个后缀的排名i和j(i<j),在height数组i+1和j之间寻找最小值。(可以用rmq优化)

  例2:最长重复子串(不重叠)(poj1743)
  解:二分长度,根据长度len分组,若某组里SA的最大值与最小值的差>=len,则说明存在长度为len的不重叠的重复子串。

  例3:最长重复子串(可重叠)
  解:height数组里的最大值。这个问题等价于求两个后缀之间的最长公共前缀。

  例4:至少重复k次的最长子串(可重叠)(poj3261)
  解:二分长度,根据长度len分组,若某组里的个数>=k,则说明存在长度为len的至少重复k次子串。

  例5:最长回文子串(ural1297)
  给定一个串,对于它的某个子串,正过来写和反过来写一样,称为回文子串。
  解:枚举每一位,计算以这个位为中心的的最长回文子串(注意串长要分奇数和偶数考虑)。将整个字符串反转写在原字符串后面,中间用$分隔。这样把问题转化为求某两个后缀的最长公共前缀。

  例6:最长公共子串(poj2774)
  给定两个字符串s1和s2,求出s1和s2的最长公共子串。
解:将s2连接到s1后,中间用$分隔开。这样就转化为求两个后缀的最长公共前缀,注意不是height里的最大值,是要满足sa[i-1]和sa[i]不能同时属于s1或者s2。

  例7:长度不小于k的公共子串的个数(poj3415)
  给定两个字符串s1和s2,求出s1和s2的长度不小于k的公共子串的个数(可以相同)。
  解:将两个字符串连接,中间用$分隔开。扫描一遍,每遇到一个s2的后缀就统计与前面的s1的后缀能产生多少个长度不小于k的公共子串,这里s1的后缀需要用单调栈来维护。然后对s1也这样做一次。

  例8:至少出现在k个串中的最长子串(poj3294)
  给定n个字符串,求至少出现在n个串中k个的最长子串。
  将n个字符串连接起来,中间用$分隔开。二分长度,根据长度len分组,判断每组的后缀是否出现在不小于k个原串中。

  相关文章:

  1. http://www.geeksforgeeks.org/suffix-array-set-1-introduction/

时间: 2024-10-13 05:19:44

【经典数据结构】后缀数组的相关文章

数据结构之后缀数组

1. 概述 后缀数组是一种解决字符串问题的有力工具.相比于后缀树,它更易于实现且占用内存更少.在实际应用中,后缀数组经常用于解决字符串有关的复杂问题. 本文大部分内容摘自参考资料[1][2]. 2. 后缀数组 2.1   几个概念 (1)后缀数组SA 是一个一维数组,它保存1..n 的某个排列SA[1],SA[2],--,SA[n],并且保证Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n.也就是将S 的n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入S

数据结构4——后缀数组

一.相关介绍 后缀数组 处理字符串的有力工具 可以处理后缀自动机解决不了的问题 后缀数组被称为SA,后缀自动机被称为SAM . 更详细的讲解点击

hiho一下123周 后缀数组四&#183;重复旋律

后缀数组四·重复旋律4 时间限制:5000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi平时的一大兴趣爱好就是演奏钢琴.我们知道一个音乐旋律被表示为长度为 N 的数构成的数列.小Hi在练习过很多曲子以后发现很多作品中的旋律有重复的部分. 我们把一段旋律称为(k,l)-重复的,如果它满足由一个长度为l的字符串重复了k次组成. 如旋律abaabaabaaba是(4,3)重复的,因为它由aba重复4次组成. 小Hi想知道一部作品中k最大的(k,l)-重复旋律. 解题方法提示 输入 一

利用后缀数组构造后缀树

由于蒟蒻azui前段时间忙着准备省选,并在省选中闷声滚大粗,博客停更了好久.. 省选过后整个人各种颓,整天玩玩泥巴什么的... 前段时间学后缀数组的时候上网查相关资料,看到说后缀数组和后缀树是可以相互转化的,并且uoj上有大量通过后缀自动机建出后缀树然后dfs遍历获得后缀数组的模板,但是通过后缀数组来建后缀树的资料确实稀缺. 也许大牛们都觉得这xjbYY一下就可以写了,所以网上没找到对应的代码,那么我来补个坑吧.大牛勿喷.. 先谈谈我的理解吧.. 讲道理后缀数组和后缀树应该是完全等价的,但前两者

hihoCoder 后缀数组 重复旋律

#1403 : 后缀数组一·重复旋律 时间限制:5000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi平时的一大兴趣爱好就是演奏钢琴.我们知道一个音乐旋律被表示为长度为 N 的数构成的数列. 小Hi在练习过很多曲子以后发现很多作品自身包含一样的旋律.旋律是一段连续的数列,相似的旋律在原数列可重叠.比如在1 2 3 2 3 2 1 中 2 3 2 出现了两次. 小Hi想知道一段旋律中出现次数至少为K次的旋律最长是多少? 解题方法提示 输入 第一行两个整数 N和K.1≤N≤2000

后缀数组之hihocoder 重复旋律1-4

蒟蒻知道今天才会打后缀数组,而且还是nlogn^2的...但基本上还是跑得过的: 重复旋律1: 二分答案,把height划分集合,height<mid就重新划分,这样保证了每个集合中的LCP>=mid,套路板子题 // MADE BY QT666 #include<cstdio> #include<algorithm> #include<cmath> #include<iostream> #include<cstring> using

后缀数组(一堆干货)

其实就是将两篇论文里的东西整合在了一起,并且提供了一个比较好理解的板. 后缀数组 字符串:一个字符串S是将n个字符顺次排列形成的数组,n称为S的长度,表示为len(S).S的第i个字符表示为S[i]. 子串:字符串S的子串S[i…j],i<=j,表示从S串中从i到j这一段,也就是顺次排列S[i],S[i+1],……,S[j]形成的字符串. 后缀:后缀是指从某个位置i开始到整个字符串末尾结束的一个特殊子串.字符串S的从i开关的后缀表示为Suffix(S,i),也就是Suffix(S,i)=S[i…

后缀数组-Codevs1500

后缀数组的原理很简单,将一个字符串S的所有后缀组成一个字符串数组,并排序,则以后每次判断某个字符串D是不是S的子串只需要strlen(D)*log(strlen(s))的时间复杂度.Codevs1500这题就是一道裸题,考后缀数组的生成,如果一个一个将S的后缀加入到后缀数组中并快速排序需要的时间复杂度为(n的平方*log n),效率极低.一般可以用倍增法将生成后缀数组的时间复杂度优化到(n*log n*log n).      倍增法的思想一开始觉得有点难理解,先只比较每个后缀的第一个字母,得到

HDU 1403 Longest Common Substring(后缀数组,最长公共子串)

hdu题目 poj题目 参考了 罗穗骞的论文<后缀数组——处理字符串的有力工具> 题意:求两个序列的最长公共子串 思路:后缀数组经典题目之一(模版题) //后缀数组sa:将s的n个后缀从小到大排序后将 排序后的后缀的开头位置 顺次放入sa中,则sa[i]储存的是排第i大的后缀的开头位置.简单的记忆就是“排第几的是谁”. //名次数组rank:rank[i]保存的是suffix(i){后缀}在所有后缀中从小到大排列的名次.则 若 sa[i]=j,则 rank[j]=i.简单的记忆就是“你排第几”