后缀数组与AC自动机的区别:后缀数组对文本串预处理,而AC自动机对匹配串处理
过程
假定文本串为BANANA,在它的末尾添加一个字符'$'(可以是任意一个未在串中出现过的字符),然后将它的所有后缀(BANANA$,ANANA$,NANA$,ANA$,NA$,A$)插到一棵(trie)中. 由于添加了尾字符'$',因此(trie)中的每一个叶节点唯一对应字符串中的一个后缀.
在绘制(trie)的过程中,对于每一个结点,我们将它的子节点按照字典序排好,字典序大的在右边,小的在左边,$规定为最小的字符,然后在叶节点中标上该结点对应后缀的第一个字符的下标(在这里默认字符串中第一个字符的下标为0),然后自左向右地将叶子的编号排出来,就得到了后缀数组(SA).
字符串BANANA$的SA={5,3,1,0,4,2}
那么怎么得到这个数组呢?我们采用Manber和Myers发明的倍增法:
首先,将所有字符排序,计算出每个字符的名次,最小的字母是第一名,次小的字母是第二名,以此类推.
例如字符串aabaaaab,对应着数组{1,1,2,1,1,1,1,2}
然后给所有后缀的前两个字符排序
对于aabaaaab来说,就是给{aa,ab,ba,aa,aa,aa,ab,b_}排序,等价于给二元组{11,12,21,11,11,11,12,20}排序
如果你没做错,你会得到{1,2,4,1,1,1,2,3}
下面给所有后缀的前四个字符排序,这等价于给数组{14,21,41,11,12,13,20,30}排序
不难发现,因为s.substr(k,4)=s.substr(k,2)+s.substr(k+2,2),因此只要将第(k)个数拼上第(k+2)个数,就是我们想要的数组
这一步排完序后,我们得到{4,6,8,1,2,3,5,7}
最后,我们给所有后缀的前8个字符排序,得到{4,6,8,1,2,3,5,7}
机智的你或许一经发现,这一步排完之后,和上一步排完之后得到一样的结果,为什么呢?因为上一步的结果中,所有的数已经互不相同,这时候再将第(k)个数加上第(k+4)个数时,由于前面一位互不相同,已经可以将所有数排好序,所以结果不会改变
下面来看看它的时间复杂度.由于每次长度翻倍,最多进行(logn)次排序,每次可以使用一些例如基数排序之类的算法,将排序复杂度降为(O(n)),所以总的复杂度为(O(nlogn)).
下面是构建SA的代码:
char s[MAXN];
int sa[MAXN],t[MAXN],t2[MAXN],c[MAXN],n;
void build_sa(int m){
int i,*x=t,*y=t2;
for(i=0;i<m;i++) c[i]=0;
for(i=0;i<n;i++) c[x[i]=s[i]]++;
for(i=1;i<m;i++) c[i]+=c[i-1];
for(i=n-1;i>=0;i--) sa[--c[x[i]]]=i;
for(int k=1;k<=n;k<<=1){
int p=0;
for(i=n-k;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
for(i=0;i<m;i++) c[i]=0;
for(i=0;i<n;i++) c[x[y[i]]]++;
for(i=0;i<m;i++) c[i]+=c[i-1];
for(i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
swap(x,y);
p=1;x[sa[0]]=0;
for(i=1;i<n;i++)
x[sa[i]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++;
if(p>=n) break;
m=p;
}
}