【后缀数组】【字符串】【数据结构】后缀数组小结

后缀数组小结

后缀数组是啥?

简而言之,后缀数组的基础内容就是对于给定的一个字符串,将其后缀按字典序排序后所形成的一个编号序列。

例如:

对于字符串ababa:

其后缀数组为SA[]={5,3,1,4,2} ,因为a < aba < ababa <ba <baba

接下来介绍一下如何求suffix array及其衍生数组(rank,height)等。

后缀数组如何求?

最朴素的想法是直接提取所有的后缀并排序,使用快速排序的复杂度为\(O(n\log n * T (cmp))\ =\ O (n^2 log n)\).

考虑如何优化这一过程。

我们观察字符串的比较过程,都是从首位开始进行比较的。

考虑一种比较方式,每次利用每个后缀的第1位先比较,然后确定大致位置,再分组在组内比较第2位,这显然是一种可取的思想,如果使用基数排序是O(n)的,否则是\(O(n \log n)\)的。考虑后缀的特性,发现了什么?

我们每一次比较都在做与之前相同的事,通俗的解释一下就是,假设第i个后缀与第j个后缀的首字母相同,事实上我接下来就是在比较第i+1个后缀与第j+1个后缀的首字母,而这个相对顺序,在上一轮排序中,我应该已经知道了才对。利用这一想法,发现第二轮比较的事物可以结合上一轮进行,然后我们又发现了什么?第二轮比较之后,事实上我获得的是每个后缀的前2个字母的相对顺序,那么由此,我就可以得到前4个,前8个……

这是一种典型的倍增思想,也是十分适合于解决求后缀数组,下面介绍一下倍增法求后缀数组的具体实现。

首先,在上述的讨论中,我们已经发现了,首先预处理出每个后缀首字母的rank,然后开始倍增:第i轮,拥有的信息是所有后缀的前\(2^{i-1}\)首字母的相对顺序,要求的是前\(2^i\)的相对顺序,对于一个后缀j,我们考虑要利用的是其上一轮本身的rank,以及后缀\(j+2^{i-1}\)上一轮的rank,显然是一个双关键字排序的过程,第一关键字为本身的rank,第二关键字为后缀\(j+2^{i-1}\)上一轮的rank,然后去重,直至每个后缀都拥有其独有的rank位置,显然总复杂度为\(O(n \log n T(cmp))\)当使用快速排序时,为\(O(n \log^2 n)\),尽管这样以及足够优秀,但是仍不足以满足对效率的要求,事实上,考虑每轮rank数不超过n,使用基数排序就可以优化对于排序的效率要求,复杂度转变为\(O(n\log n)\).

由于网络上的代码大多简单难懂,缺乏注释,先来看一个比较直观好理解的代码(by 学弟 Pink_Rabbit)

#include<cstdio>
#include<cstring>
int len, sig=127;
char str[100005];
int rank[100005], rk2[100005];
int buk[100005], buk2[100005], nxt[100005], to[100005], tot;
inline void ins(int x,int y){nxt[++tot]=buk[x];to[tot]=y;buk[x]=tot;}
inline void ins2(int x,int y){nxt[++tot]=buk2[x];to[tot]=y;buk2[x]=tot;}
int SA[100005];
void getSA(){
	for(int i=1;i<=len;++i) rank[i]=str[i]; 
	for(int j=1,p;j<=len;j<<=1){
		tot=p=0;
		memset(buk,-1,sizeof buk);
		for(int i=1;i<=len;++i)
			rk2[i] = i+j<=len ? rank[i+j] : 0,
			ins(rk2[i],i);
		memset(buk2,-1,sizeof buk2);
		for(int i=sig;i>=0;--i){
			for(int k=buk[i];~k;k=nxt[k]){
				ins2(rank[to[k]],to[k]);
			}
		}
		for(int i=0;i<=sig;++i){
			int lst=-1;
			for(int k=buk2[i];~k;k=nxt[k]){
				if(rk2[to[k]]==lst) rank[to[k]]=p;
				else rank[to[k]]=++p, lst=rk2[to[k]];
			}
		}
		sig=p;
		for(int i=1;i<=len;++i) printf("%d ",rank[i]); puts("");
	}
	for(int i=1;i<=len;++i) SA[rank[i]]=i;
	for(int i=1;i<=len;++i) printf("%d ",SA[i]); puts("");
}
int main(){
	scanf("%s",str+1);
	len=strlen(str+1);
	getSA();
	return 0;
}
/*使用链表是最直观的书写方式,如果对于网上的绝大多数模板不甚理解,建议这样写,虽然常数大,但这是最直观的写法。*/

接下来给出一个常见的模板

/*
	poj 2774 SA
	copyright (c) Melacau.
	All rights reserved.
*/
/*
	task:
		Given 2 string(1<=|S|<=10^5)
		Ask about the max length of the same substrng of the two string.
	solution:
		1.Get a new string s = a+" "+b
		2.Get height of s
		3.Get the max height[i] in case of i belongs to a and i+1 belongs to b or ...
*/
#include <stdio.h>
#include <string.h>
#define R register
#define MN 200005
inline int max(int a,int b){return a>b?a:b;}
int sa[2][MN],rk[2][MN],num[MN],vl[MN],n,g,la,lb,ans,height[MN];char a[MN],b[MN];
inline void getsa(int *sa,int *rk,int *SA,int *RK,R int k){
	R int i;for (i=1; i<=n; ++i) num[rk[sa[i]]]=i;//base sort 1st key
	for (i=n; i; --i) if (sa[i]>k) SA[num[rk[sa[i]-k]]--]=sa[i]-k;//get sa in [1..n-k] based on 2nd key(loop) and 1st key(num[])
	for (i=n-k; ++i<=n; ) SA[num[rk[i]]--]=i;//get sa in (n-k,n]
	for (i=1; i<=n; ++i) RK[SA[i]]=RK[SA[i-1]]+(rk[SA[i]]!=rk[SA[i-1]]||rk[SA[i]+k]!=rk[SA[i-1]+k]);//get all new rk
}
void build(){
	R int i;for (i=1; i<=n; ++i) ++num[vl[i]];
	for (i=1; i<=27; ++i) num[i]+=num[i-1];//get base sort R1
	for (i=1; i<=n; ++i) sa[0][num[vl[i]]--]=i;//get sa R1
	for (i=1; i<=n; ++i) rk[0][sa[0][i]]=rk[0][sa[0][i-1]]+(vl[sa[0][i]]!=vl[sa[0][i-1]]);//get all rk R1
	for (i=1; rk[g][sa[g][n]]<n; i<<=1,g^=1) getsa(sa[g],rk[g],sa[g^1],rk[g^1],i);//multiply get sa
}
void get_height(){
	R int i,j,k=0;for (i=1;i<=n;++i)	
		if (rk[g][i]!=1){
			j=sa[g][rk[g][i]-1];//as h[i]>=h[i-1]-1 get pos
			while(vl[i+k]==vl[j+k]) ++k;//get the length of height
			height[rk[g][i]]=k;if (k) --k;//for h[i]>=h[i-1]-1 , if k > 0 then --k
		}
}
int main(){
	R int i;
	scanf("%s",a+1);la=strlen(a+1);
	scanf("%s",b+1);lb=strlen(b+1);
	for (i=1; i<=la; ++i) vl[i]=a[i]-'a'+1;
	vl[la+1]=27;n=la+lb+1;
	for (i=la+2; i<=n; ++i) vl[i]=b[i-la-1]-'a'+1;
	build();get_height();
	for (R int i=2; i<=n; ++i)
		if (sa[g][i]<=la&&sa[g][i-1]>la+1||sa[g][i]>la+1&&sa[g][i-1]<=la)
			ans=max(ans,height[i]);
	printf("%d\n",ans);
	return 0;
}

接下来对上述代码分段解释:

for (i=1; i<=n; ++i) ++num[vl[i]];
for (i=1; i<=27; ++i) num[i]+=num[i-1];
for (i=1; i<=n; ++i) sa[0][num[vl[i]]--]=i;
for (i=1; i<=n; ++i) rk[0][sa[0][i]]=rk[0][sa[0][i-1]]+(vl[sa[0][i]]!=vl[sa[0][i-1]]);

首先第1行,典型的计数统计;

其次第2行,是为了计算每个字母的排名范围;

接下来第3行是比较难理解的,首先明确一点,这里的sa意味着,排名对应的后缀的起始位置,因此,这句话的实质就是基数排序。简单的解释一下就是,我现在给你个数字,这个数字对应的排名区间你已经知道了,所以这个区间内的某个排名就对应了这个数的位置,现在枚举数,求排名,手算或者观察还是很好理解的。

第4行是算每个位置的排名,不多解释。

接下来看倍增部分,外层循环不说,滚动数组优化,使用传指针的方式简化代码,提高效率只是技巧,观察实现本身:

inline void getsa(int *sa,int *rk,int *SA,int *RK,R int k){
	R int i;for (i=1; i<=n; ++i) num[rk[sa[i]]]=i;
	for (i=n; i; --i) if (sa[i]>k) SA[num[rk[sa[i]-k]]--]=sa[i]-k;
	for (i=n-k; ++i<=n; ) SA[num[rk[i]]--]=i;
	for (i=1; i<=n; ++i) RK[SA[i]]=RK[SA[i-1]]+(rk[SA[i]]!=rk[SA[i-1]]||rk[SA[i]+k]!=rk[SA[i-1]+k]);
}

首先解释第1行,这样基数排序的目的是,对上一次的rank,计算对应的区间(为什么?)这层循环的意思就是找到每个rank对应的终止排名。

接下来看第2行,考虑原本的第i名后缀j,如果它可以作为新一轮的第2关键字的话,由于循环是倒序的,显然相同第一关键字越早出现的被分配到的排名越后,这是符合实现的,因此按照这个顺序,对存在第2关键字的后缀分配排名。

什么,还有没有第2关键字的?显然是存在的,即后缀本身长度已经不大于当前的倍增长度的,我们认为他们的第2关键字为0,这也就是第3行的作用,对这部分后缀分配新排名。

注意:第2、3两行是无法颠倒顺序的,第2行循环的顺序也很重要,原因是这是基于第2关键字的实现。

最后第4行,计算排名,浅显易懂。

至此,我们已经完成了SA的计算与rank的计算。

辅助数组height

接下来,讲一下一个辅助数组,height的作用:

它的定义是,height[i]表示排名为i的后缀与排名为i-1的后缀的最长公共前缀。

显然暴力求的话是\(O(n^2)\)十分睿智。

介绍两个性质:

\(height[i] >= height[rk[sa[i]-1]]-1\)

\(|lcp(suffix[i],suffix[j])|=min\{ height[k] \}(rk[i] \leq k \leq rk[j])\)

性质2比较显然。

下面给出性质1证明:

如果height[rk[sa[i]-1]]=1/0,这是显然成立的。

分析大于1的情况:

首先设sa[rk[sa[i]-1]-1]=k

显然suffix[k]与suffix[sa[i]-1]在去掉首字母之后仍有公共前缀,此时他们分别是suffix[k+1],suffix[sa[i]],那么这两个后缀的最长公共前缀的长度显然等于height[rk[sa[i]-1]]-1。又根据性质2,则有性质1。

于是,根据性质2,我们就比较容易写出代码:

void get_height(){
	R int i,j,k=0;for (i=1;i<=n;++i)	
		if (rk[g][i]!=1){
			j=sa[g][rk[g][i]-1];
			while(vl[i+k]==vl[j+k]) ++k;
			height[rk[g][i]]=k;if (k) --k;
		}
}

总结

利用这些数组,我们就可以比较轻松的解决一些字符串问题,说白了,后缀数组及其辅助数组height不过是一个工具,具体如何使用还需大家自行琢磨。本文只是简单的解释了一下后缀数组模板的首先及代码书写的缘由,并不准备介绍例题,给出的模板是poj 2774 查询两个字符串的lcs。可以结合代码自行理解使用技巧。

posted @ 2018-01-24 14:33  Melacau  阅读(448)  评论(0编辑  收藏  举报