后缀数组 代码详解

此生再无相见时 提交于 2020-02-17 02:14:22

研究了好几天的后缀数组,今天终于是把代码实现给看明白了了。

大多数博客都讲解了了后缀数组以及倍增法,但是对于代码的讲解不是很明白。

这篇就把LRJ蓝书上的代码拆开来一句一句的进行解释。

关于倍增算法我建议去看lrj的数,写的非常清晰明了;基数排序可以去看看百度百科,其实和桶排序是一家子。

代码:

char s[maxn];
int sa[maxn],t[maxn],t2[maxn],c[maxn],n;
void print(int *arr){
	for(int i=0;i<n;i++) cout<<arr[i]<<" ";
	cout<<endl;
}
void getSa(int m){
	int *x=t,*y=t2;
	for(int i=0;i<m;i++) c[i]=0;
	for(int i=0;i<n;i++) c[x[i]=s[i]]++;
	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;
	cout<<"sa:";print(sa);
	cout<<"x:";print(x);
	
	
	for(int k=1;k<=n;k<<=1){
		int p=0;
		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;
		cout<<"y:";print(y);
		
		for(int i=0;i<m;i++) c[i]=0;
		for(int i=0;i<n;i++)
		 	c[x[y[i]]]++;
		for(int i=0;i<m;i++) c[i]+=c[i-1];
		for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
		cout<<"sa:";print(sa);
		
		swap(x,y);
		cout<<"y:";print(y);
		p=1;x[sa[0]]=0; 
		for(int 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++;
		cout<<"x:";print(x);
		if(p>=n) break;
		m=p; 
	}
}

代码中为了便于调试增加了数组输出的函数;

首先我们必须搞懂每一个数组的意义和用途,这是非常重要的而且是书上所没有提及的。

不搞懂容易让人一头雾水。

sa y c s x
后缀数组(第一关键字的名次) 第二关键字的名次 基数排序用到的“桶” 字符串 后缀数组的一个离散

 

 

 

上表用于在完全理解了几个数组的含义后对照使用,下面在深度的说一下这几个数组

sa:就是最后要得到的后缀数组,也就是第一关键字的名次,第一关键字的定义的蓝书上非常明了,可以参看书上。

sa[i]:排名为i的后缀的下标为sa[i]

x:很多人把x数组当做第一关键字的排序,我觉得这么理解的话容易让人产生误解。

我觉得把x理解为后缀的一个离散化数组更好。至于怎么个离散化,一会看代码;

比如第一遍倍增法的得到的后缀是这样的(书上例子):aa,ab,ba,aa,aa,aa,ab,b$;

那么对应的x数组为:0 1 3 0 0 0 1 2

x[i]:后缀i的离散化;

y:以第二关键字进行排序,排名第i名的后缀为y[i]

                                                                                                                                                                           

下面看具体代码:

先看这四行:

	for(int i=0;i<m;i++) c[i]=0;
	for(int i=0;i<n;i++) c[x[i]=s[i]]++;
	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 i=0;i<n;i++) c[x[i]=s[i]]++;

将s[i]赋值到x[i]中,并放入桶中。

此时x数组为:97 97 98 97 97 97 97 97 98;c数组为:c[97]=6;c[98]=2;其余为0

(97 97分别为a,b对应的ASC码值)

for(int i=1;i<m;i++) c[i]+=c[i-1];

加上前面桶的值,也就得到了这个值的排名;c[97]=6;c[98]=8;

for(int i=n-1;i>=0;i--) sa[--c[x[i]]]=i;

这一行稍微难理解一点,再重申一下sa的定义:sa[i]:排名为i的后缀;

在上一行代码中,对应的桶中已经记录了对应“字符”的排名(字符离散到x数组中)

所以说每个排名对应的下面就全部放入到sa之中了;

eg:sa[--c[x[7]]=7->sa[--c[98]]=7->sa[7]=7 所以排第七的是后缀7

--c[x[i]]的意思是这个元素已经拿走了,下一次再取到和这个相同的元素的话排名会提前一个,所以要--

然后就是这个循环的顺序问题,他是从大到小的。想一下为什么,可以反过来吗?

提示一下这个c[i]的值是有关系的, 因为c[i]在取完之后是减一,这就保证了对于相同后缀,后面的排名更高。

                                                                                                                                                                                     

然后看下面这堆代码

	for(int k=1;k<=n;k<<=1){       //k是倍增的‘步数’
		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;
		cout<<"y:";print(y);
		
		for(int i=0;i<m;i++) c[i]=0;
		for(int i=0;i<n;i++) c[x[y[i]]]++;
		for(int i=0;i<m;i++) c[i]+=c[i-1];
		for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
		cout<<"sa:";print(sa);
		
		swap(x,y);
		cout<<"y:";print(y);
		p=1;x[sa[0]]=0; 
		for(int 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++;
		cout<<"x:";print(x);
		if(p>=n) break;
		m=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;

这两行是求y数组,第二关键字的排序,向上翻看y的定义并牢牢记住,非常重要,不要容易把自己绕进去;

第一行:后面k个后缀是没有第二关键字的,所以他们的排名在前面,参照课本很容易理解;

第二行:根据sa求y,也就是说根据第一关键字求第二关键字;

这很容易理解,也很容易想,当前位置的第一关键字,不就是前一位置的第二关键字嘛,对吧

根据代码实际动手构造一下会很容易理解;sa[i]>=k是因为前k个字符根本不能成为任何后缀的第二关键字;

sa[i]-k是因为当前下标是由后面下标提过来的,需要-k;(建议手动理解)

看下面四行代码

for(int i=0;i<m;i++) c[i]=0;
for(int i=0;i<n;i++) c[x[y[i]]]++;
for(int i=0;i<m;i++) c[i]+=c[i-1];
for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];

看上去和刚开始的四行一样,但是有有点不一样;

就是第二行和第四行的i换成了y[i],也可以这样理解,刚开始没有第二关键字,所以y就是从0开始递增的,也就是y[i]=i;

对于相同的不作过多的赘述

看这一行:

for(int i=0;i<n;i++) c[x[y[i]]]++;

那么x[y[i]]表示啥意思呢?就是一第二关键字为顺序组成的一个离散化的数组

这个数组第二关键字是有序的。

建议把这个数组写一遍,就能发现其含义;

再看第四行

for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];

 从上面知道了,x[y[i]]的第二关键字是有序的,那么再把第一关键字排一下,那么两个不就都有序了吗

同样,顺序是从大到小的,原因同上;

 

到这里,比较难懂的地方就完成了,细节比较多,不容易一次性理解。

剩下的地方看着书很容易理解,代码也容易书写,就不做过多解释了。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!