零基础后缀数组

后缀数组的定义

基本定义在百度里都有,就不详细说了
百度百科:传送门

详讲后缀数组

首先来看一下数组的定义:
1、sa[i]=j ,构造完成前表示关键字数组,i表示名次,j表示关键字的首字符位置,值相同的时候名次根据在原串中相对位置的先后决定;构造完成后表示后缀数组,i表示名次,j表示后缀的首字符位置。
2、rank[i]=j (代码中用x[]代替),i表示关键字的位置,j表示关键字大小。
3、y[i]=j,排序后的第二关键字数组,i表示名次,j代表第二关键字的首字符位置。
4、w[i]=j,没相加前缀和之前表示第一关键字为 i 的数,有 j 个。

倍增算法

时间复杂度为O(nlogn)

基数排序

先来讲一下基数排序,如果我们使用快速排序的话,我们每一次构造rank数组的时间复杂度为O(nlongn),用基数排序的时间复杂度为O(n),所以基数排序是倍增算法的核心。

特点:先根据x的值排序,x值相等时根据出现先后次序排序。

代码如下:

int n = strlen(a);//字符串大小

for (i = 0; i < 128; i++) w[i] = 0;//初始化

for (i = 0; i < n; i++) w[x[i] = a[i]]++;//将字符转化为ascll码的形式储存

for (i = 1; i < 128; i++) w[i] += w[i - 1];//根据首字母排序,首字母为i的前面留出首字母为1~i-1的数足够位置

for (i = n - 1; i >= 0; i--) sa[--w[x[i]]] = i;//sa[i]表示排名为i的串首字母在原串中的位置

举个例子:

字符串:aabaaaab
对应位:12345678
个数:a:6,b:2
前缀和:a:6,b:8
(这里解释的是w[i]+=w[i-1])

所以a的范围为0 ~ 5,b的范围为6 ~ 7
w数组中每个字母对应的个数为:
a b
6 2

经过w[i] += w[i - 1];
w数组对应的字符及大小:
a b
6 8

解释一下这个for (i = n - 1; i >= 0; i--) sa[--w[x[i]]] = i;

i=7 sa[7]=7 (x[i]=b,w[x[i]]=8)
i=6 sa[5]=6 (x[i]=a,w[x[i]]=6)
i=5 sa[4]=5 (x[i]=a,w[x[i]]=5)
i=4 sa[3]=4 (x[i]=a,w[x[i]]=4)
i=3 sa[2]=3 (x[i]=a,w[x[i]]=3)
i=2 sa[6]=2 (x[i]=b,w[x[i]]=7)
i=1 sa[1]=1 (x[i]=a,w[x[i]]=2)
i=0 sa[0]=0 (x[i]=a,w[x[i]]=1)

所以字符串及其对应的排序为:
a a b a a a a b
1 2 7 3 4 5 6 8

倍增算法过程

在这里插入图片描述
首先读入字符串之后,先对字符串中的单个字符按照字典序进行排序,排序完的名次作为第一关键字。

之后合并第一关键字和第二关键字再进行基数排序,,

再进行无数次基数排序。

直到所有后缀的排名都不想等,也就是相对大小可以确定,结束。

这里有个小优化,基数排序需要分成两次,第一次是对第二关键字进行排序,第二次是对第一关键字进行排序,而上一次求出的sa值就是第二关键字的排序

第二关键字的排序代码如下:

for (int i = n - k; i < n; i++) y[p++] = i;//y为第二关键字的排序,k为当前字符串的长度

for (int i = 0; i < n; i++) if (sa[i] >= k) y[p++] = sa[i] - k;

解释for (int i = n - k; i < n; i++) y[p++] = i;

先将后几位存进y数组里,在y数组中,值保存的是他们的位置(和sa数组一样)
根据图来看,每一次倍增之后,都是数组的后k位都是没有第二关键字的,所以第二关键字默认为0
因为0是作为第二关键字最小的数,所以根据他们现在的排序(由n-k-1~n-1),将他们放进数组y中,不会影响到他们的排序。

解释for (int i = 0; i < n; i++) if (sa[i] >= k) y[p++] = sa[i] - k;

sa数组的下标为排名,sa数组存的是字母的位置。
在上一个for循环中已经将后几个没有第二关键字(第二关键字为0)的位置,存进数组了,这一步是要将有第二关键字的位置存进数组。

先说一下为什么sa[i]>=k

在下图的这层循环里k=2,3和4不能加进y数组里,因为这一步骤是对第二关键字的排序,3和4的位置分别为0和1,他们不在下图的第二关键字里,所以他们不加进y数组。

sa[i]-k

位置0的第一关键字为3,第二关键字为2,他本身在位置0上,而他的第二关键字在位置2上,所以需要-2,2就是k的值,所以他需要减k。

这样就已经将所有的第二关键字都存进y数组了,下面就开始对y数组进行排序了。

图中的第一关键字和第二关键字说的是下面X Y那一排的
第一关键字的排序代码如下:

for (int i = 0; i < m; i++) w[i] = 0;

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

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

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

w[x[y[i]]]++; 这一步有很多人不理解

y[i]存的是位置,在上一步中,先把没有第二关键字(第二关键字为0)的位置存进来了,i表示的是排名,因为他们的第二关键字为0,又是按顺序存的,所以前k个是排好序的,后n-k个存的之前第一关键字的位置-k,因为他们在之前是按第一关键字存的,减k也不影响,所以可以说y数组中的数都是按之前第一关键字排好序的,所以当前y数组的下标就是当前第二关键字的顺序,y数组的值就是他们的位置。

x数组在之前存的是每个字母的ascll值,w数组就是桶。

比如aabaaaab这个字符串
for (int i = 0; i < n; i++) w[x[y[i]]]++;
i=0 y[i]=7 x[y[i]]=98 w[x[y[i]]]=1
i=1 y[i]=0 x[y[i]]=97 w[x[y[i]]]=1
i=2 y[i]=2 x[y[i]]=98 w[x[y[i]]]=2
i=3 y[i]=3 x[y[i]]=97 w[x[y[i]]]=2
i=4 y[i]=4 x[y[i]]=97 w[x[y[i]]]=3
i=5 y[i]=5 x[y[i]]=97 w[x[y[i]]]=4
i=6 y[i]=1 x[y[i]]=97 w[x[y[i]]]=5
i=7 y[i]=6 x[y[i]]=97 w[x[y[i]]]=6

w[97]=6(装桶后a的数量)
w[98]=2(装桶后b的数量)

之后进行基数排序就和上面一样了。

以上就是求sa的过程,求出sa之后就可以求出rank,可能存在多个字符串的rank值相同,所以必须比较两个字符串是否完全相同。
比较代码:

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++;

针对两个串比较直边所指元素和斜边所指元素
每个串都彼此大小不同了,事实上后缀就是应该所有都不相等的,相对大小已确定,退出循环
p为不同的字符串的个数,如果p=n,那么就可以退出循环了

if(p>=n)break;

因为y数组的值已经没用了,所以为了节省空间,可以用y数组保存rank值。
一个小优化,将x,y定义为指针类型,复制数组可以用指针来代替。
这里还有一个函数swap(),用来交换两个数组。

swap(x, y);

完整代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
const int MAXascll = 128;
char s[MAXN];
int sa[MAXN], t1[MAXN], t2[MAXN], w[MAXN], n;
void Output() {
	for (int i = 0; i < n; i++) printf("%d ", sa[i]);
	printf("\n");
}
void build(int m) {
	int* x = t1, * y = t2;
	for (int i = 0; i < m; i++) w[i] = 0;
	for (int i = 0; i < n; i++) w[x[i] = s[i]]++;
	for (int i = 1; i < m; i++) w[i] += w[i - 1];
	for (int i = n - 1; i >= 0; i--) sa[--w[x[i]]] = i;
	for (int k = 1; k <= n; k = 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;
		for (int i = 0; i < m; i++) w[i] = 0;
		for (int i = 0; i < n; i++) w[x[y[i]]]++;
		for (int i = 0; i < m; i++) w[i] += w[i - 1];
		for (int i = n - 1; i >= 0; i--) sa[--w[x[y[i]]]] = y[i];
		swap(x, 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++;
		if (p >= n) break;
		m = p;
	}
}
int main() {
	scanf("%s", s);
	n = strlen(s);
	build(MAXascll);
	Output();
	return 0;
}

height数组日后再更~

后缀数组的应用

1.最长公共前缀
2.单个字符串的相关问题
3.重复子串
3.1.子串个数
3.2.回文子串
3.3.连续重复子串
4.两个字符串的相关问题
5.多个字符串的相关问题
以上问题都可以在下面的论文中找到(日后再补)

写的很好的后缀数组论文

国家集训队2009论文集后缀数组——处理字符:传送门

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值