后缀数组SA

后缀数组SA

后缀数组大多用来处理字符串问题,主要能够求字符串子串的LCP(最长公共前缀)

分类:字符串算法

后缀排序

这里讲述的是倍增法,如果下文有些地方文字描述不是很准确(比如没有写减一)是因为作者懒得想那么多了,感性理解一下即可

前置知识

  • 基数排序(不会的建议简单学习整数的基数排序)

后缀排序即把字符串的每个后缀按照字典序排序,便于后面求解LCP

首先我们考虑一种暴力做法:把每个后缀拿出来然后sort,复杂度 \(O(n^2logn)\)

我们可以发现每两个字符串的比较是从前往后的,基数排序也是从前往后比较的且每一位只比较了一遍,所以如果使用基数排序可以可以减少大量的运算,复杂度 \(O(n^2)\),这里不给出代码

观察这些字符串的性质可以发现,第i个后缀的第x位刚好是第i + x个后缀的第一位,根据这个性质,我们可以用类似记忆化的方法来加速排序

我们知道基数排序可以可以相当于每次取两个关键字进行排序,第i次排序的两个关键字分别为上一次排序得到的排名和这个字符串第i位上的值

现在,我们把基数排序第i次排序的两个关键字变为上一次排序的排名(即这个后缀的第 \(1​\) 位到第\(2^{i-1}​\)位)和这个后缀后面的第 \(2^{i-1}​\)个后缀上一次排序的排名(即这个后缀的第 \(2^{i - 1}+1​\) 位到第 \(2^{i}​\) 位),这样排序显然是正确的,类似于倍增,但是要注意的一点是对于两个相同的字符串要赋予相同的排名,否则两个前 \(2^k \ (k\in Z)​\) 个字符相同的后缀的排名可能会错乱

由于非常的难看,这里就不放出这个代码了,如果自己写可以发现这样写的复杂度是满的,非常慢,而且常数好像有点大

这里声明几个变量的含义:

  • s:原字符串
  • sa:排名为i的后缀的第一个字母在原字符串中的位置,即排名为i的后缀
  • rk:第i个后缀的排名
  • y:第二关键字排名为i的后缀的第一个字符在原字符串中的位置
  • bot:桶
  • pw:当前倍增到的长度,即\(2^{倍增次数-1}​\)

先给出一个简单的基数排序的代码

int sa[MAXN], rk[MAXN];
int bot[MAXN], y[MAXN];
int m = 0; // m 表示当前最大的排名(用于减少运算量)
void radix_sort(int n) {
	for(int i = 0; i <= m; i ++) bot[i] = 0; // 清空
	for(int i = 1; i <= n; i ++) bot[rk[i]] ++; // 放入桶
	for(int i = 1; i <= m; i ++) bot[i] += bot[i - 1]; // 记前缀和得到排名
	for(int i = n; i >= 1; i --) sa[bot[rk[y[i]]] --] = y[i]; // ???
}

由于这个对于基数排序新手来说有点看不懂这个的正确性所以解释一下

基数排序的第4行强行翻译一下:排名为((第二关键字排名为i的后缀)的第一关键字的新排名)的后缀是第二关键字排名为i的后缀(?????)

说人话:在新排名中,第二关键字排在第i位的后缀排在所有第一关键字小于它(由bot保证)或者第一关键字等于它且第二关键字小于它(由倒序枚举保证)的所有后缀的后面(感性理解一下),所以这个是正确的

每次排序前我们需要处理y数组,非常简单

cnt = 0;
for(int i = 1; i <= pw; i ++) y[++ cnt] = n - pw + i; // 这些后缀的长度小于倍增到的长度,所以不存在第二关键字,我们把它们设为最小
for(int i = 1; i <= n; i ++)
	if(sa[i] > pw) y[++ cnt] = sa[i] - pw; // 排名为i的后缀是第sa[i]-pw个后缀的第二关键字

然后每次排完后我们都要处理新的rk,特别是要保证两个相同的字符串赋予相同的排名

swap(y, rk); // 此时y数组已经没用了,而rk在后面需要被修改,这样做可以省下一个数组
// 注意下面的y的含义是第i个后缀的第一关键字的排名
rk[sa[1]] = cnt = 1; 
for(int i = 2; i <= n; i ++)
	if(y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + pw] == y[sa[i] + pw]) rk[sa[i]] = cnt; // sa[i] + w 即第二关键字的排名,这里就是两个关键字都相同就把排名设为相同的
	else rk[sa[i]] = ++ cnt;

整合一下大概是这样

#define MAXN 500003
int sa[MAXN], rk[MAXN];
int bot[MAXN], y[MAXN];
int m = 0; // m 要预先设为字符集大小
char s[MAXN + 1];
void suffix_sort(int n) {
	for(int i = 1; i <= n; i ++) rk[i] = s[i], y[i] = i;
	radix_sort(n); // 先进行一遍排序,处理单个字符
	for(int pw = 1, cnt = 0; cnt < n; pw <<= 1, m = cnt) {
		cnt = 0;
		for(int i = 1; i <= pw; i ++) y[++ cnt] = n - pw + i;
		for(int i = 1; i <= n; i ++)
			if(sa[i] > pw) y[++ cnt] = sa[i] - pw;
		for(int i = 0; i <= m; i ++) bot[i] = 0;
		for(int i = 1; i <= n; i ++) bot[rk[i]] ++;
		for(int i = 1; i <= m; i ++) bot[i] += bot[i - 1];
		for(int i = n; i >= 1; i --) sa[bot[rk[y[i]]] --] = y[i];
		swap(y, rk);
		rk[sa[1]] = cnt = 1;
		for(int i = 2; i <= n; i ++)
			if(y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + pw] == y[sa[i] + pw]) rk[sa[i]] = cnt;
			else rk[sa[i]] = ++ cnt;
	}
}

求最长公共前缀(LCP)

现在排序排玩了,但感觉没什么用?

我们将这些后缀按照排名输出来可以发现,与某个后缀的LCP最长的后缀(不包含自己)一定刚好在这个后缀的前面或者后面(显然,因为是按字典序排的)

然后这里有个性质:若LCP(i,j)>LCP(i,k)那么LCP(j,k)=LCP(i,k),若LCP(i,j)=LCP(i,k),则LCP(j,k)>=LCP(i,k)(这个性质显然成立)

然后再乱想一下:若按照字典序排序后 \(i<j<k\),那么 LCP(i,k)=min(LCP(i,j), LCP(j,k))

这样我们的后缀数组就有用武之地了,我们求出每两个排完序后的相邻后缀的LCP(记为height),然后每次询问的时候只需要取这两个后缀中间的height的最小值即可

但是我们发现求两个排完序后的相邻后缀的LCP的复杂度是 \(O(n^2 )\) 的,所以我们还需要优化

注意一个地方:因为后缀i是后缀i-1的前面删去一个字符,所以height[sa[i]] >= height[sa[i-1]] - 1

下面简单证明一下这个结论:

排在后缀i-1前面的那个后缀删去第一个字符后仍然是一个后缀(设为j),于是一定有LCP(j,i)=height[i-1]-1,这时候我们就需要讨论j的排名来确定height[i]

如果j的排名在i的前面,一定满足height[i]>=LCP(i,j),因为这时候 LCP(i,j)=\min{height[k]}(k\in (j, i])

如果j的排名在i的后面,那么LCP(i,j)=0 且 height[i]=0 或 1,这个应该很容易理解,就不解释了,又因为任意height是非负的,所以也满足height[i]>=LCP(i,j)

现在我们就可以O(1)地求height

height[1] = 0;
for(int i = 1, p = 0; i <= n; i ++, p = max(0, p - 1)) {
	if(rk[i] == 1) continue; // height[1] 不存在,设为 0
	while(i + p <= n && sa[rk[i] - 1] + p <= n && s[i + p] == s[sa[rk[i] - 1] + p]) p ++;
	height[rk[i]] = p;
}

因为不是数据结构,不好封装,使用起来会有问题,所以完整代码暂时先不放了

posted @ 2019-04-01 21:42  akakw1  阅读(278)  评论(0编辑  收藏  举报