在字符串处理当中,后缀树和后缀数组都是非常有力的工具。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。可以说,在信息学竞赛中后缀数组比后缀树要更为实用。
鉴于后缀数组的实用性,以及它的的难度,我写了这篇博文,希望能够对读者有一定的帮助。
首先了解一下有关定义:
1.子串:字符串str的字串sub[i.......j] 表示在字符串str中从i到j的一段字符串,长度为n的字符串共有(n+1)*n/2个字串
如str="abcde", "a","ab","cde","abcde"等均是str的子串;
2.后缀suffix: suffix[ i ]表示在字符串str中从i开始到字符串结尾的字串,如str="abcde",suffix[2]="cde",suffix[4]="3"。
3.后缀数组sa: 后缀数组 SA 是一个一维数组,它保存 1..n 的某个排列 SA[1 ] ,SA[2] , …… , SA[n] ,并且保证
suffix(SA[i]) < Suffix(SA[i+1]) , 1 ≤ i<n 。也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头 位置顺次放入 SA 中。即:sa[i]=j 表示排名为i的后缀在str中是以j开始的后缀,假设sa[2]=3,str="abcde" ,则表示的是后缀"cde"在所有后缀中排第2.
4.排序数组Rank:Rank[i]表示suffix[i]在所有后缀里排列的名次。简单的说,后缀数组(sa)是 “ 排第几的是谁? ” ,名次数组(Rank)是 “ 你排第几?
5.height数组:后缀数组中相邻两个后缀的最大公共前缀, height[i] 的值是 sa[i-1]和sa[i] 的公共前缀长度。
6.字符串的比较:字符串比较是按照字典序比较的
有关定义了解后,我们开始走进算法
求后缀数组最常用的方法是倍增算法,至于为什么叫倍增算法我们不必纠结。。。。
建立后缀数组采用倍增的方式, 我们需要知道以下先验知识:
其中kSuffix(j) ,表示以位置j 的后缀的长度为k的前缀
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N = 50005;
int sa[N],Rank[N],height[N],c[N];
int n,m;
char str[N];
int cmp(int r[],int a,int b,int l){
return r[a]==r[b]&&r[a+l]==r[b+l];
}
//以下为倍增算法求后缀数组
void build_SA(int n){
int x[N],y[N];
//第一次基数排序
memset(c,0,sizeof(c));
for(int i=0;i<n;i++) c[x[i]=str[i]]++; //字符串的ASCII为数组下标
for(int i=1;i<m;i++) c[i]+=c[i-1];//求前缀和
for(int i=n-1;i>=0;i--) sa[--c[x[i]]]=i;
// 倍增算法
for(int k=1;k<=n;k<<=1){
int p=0; //p是不同后缀的数量
for(int i=n-k;i<n;i++) y[p++]=i;
for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
memset(c,0,sizeof(c));
for(int i=0;i<n;i++) c[x[y[i]]]++;
for(int i=1;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
swap(x,y);
p=1;
x[sa[0]]=0;
for(int i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],k)?p-1:p++;
if(p>=n) break;
m=p;
}
}
void get_Rank(int n){
for(int i=1;i<=n;i++)
Rank[sa[i]]=i;
}
void get_height(int n){
for(int i=0,k=0;i<n;i++){
if(k) k--;
int j=sa[Rank[i]-1];
while(i+k<n&&j+k<n&&str[i+k]==str[j+k]) k++;
height[Rank[i]]=k;
}
}