后缀数组的定义
基本定义在百度里都有,就不详细说了
百度百科:传送门
详讲后缀数组
首先来看一下数组的定义:
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论文集后缀数组——处理字符:传送门