后缀数组 SA

考虑一个问题:

给出一个长度为 \(n\) 的字符串,容易知道其一共有 \(n\) 个后缀,现将这 \(n\) 个后缀进行排序,求出排序后的后缀序列。

这里给出一些定义:

子串:字符串 \(s\) 中连续的一段字符,从第 \(i\) 个字符到第 \(j\) 个字符的子串记作 \(s[i\dots j]\) ,其表示的字符串为 \(s_i,s_{i+1},s_{i+2},\dots,s_{j-1},s_j\) ,长度为 \(j-i+1\)
后缀:指右端点与原串右端点重合的子串,子串 \(s[i\dots n]\) 记作 \(\text{后缀 i}\)
关于两个字符串的大小,我们从两个字符串的第一个字符开始逐个比较,设当前比较到第 \(i\) 个字符。若 \(s1[i]>s2[i]\) ,则 \(s1>s2\) ;若当其中较短串全都比较完毕并无不同,那么看做较长的字符串较大。如若长度全都相等,则视为两个字符串相等。

考虑将后缀进行排序的问题,显然各个字符串的长度都不相同,可知每个后缀都有唯一切确定的排名。

朴素做法:

对于朴素做法,我们可以使用快速排序 \(\left(sort\right)\) 来进行处理,对于两个字符串的比较,我们进行 \(\mathcal{O}\left(n\right)\) 复杂度的比较,可以得到一个 \(\mathcal{O}\left(n^2logn\right)\) 的算法。

不用说,这个复杂度显然不够优秀。

乱搞做法

我们可以想办法优化一下字符串比较,实际上对于两个字符串,我们可以将他们分为两部分,分别是前一段的相同区域以及后一段的不同区域。我们可以二分找出两个区域的分界处,使用哈希处理即可,复杂度是 \(\mathcal{O}\left(nlog^2n\right)\) 的。

倍增做法

我们使用倍增的方法比较字符串的大小,并使用基数排序的方法,最后可以达到一个比较优秀的复杂度 \(\mathcal{O}\left(nlogn\right)\)

我们的主体思路是我们将从每一个字符开始长度为 \(2^k\) 的字符串进行排序,枚举 \(k\) 将字符串长度进行倍增,最后得到所有后缀的排序。

我们考虑基数排序,每个元素有两个关键字,并且关键字的值域并不大,可以使用桶来处理。我们首先枚举第二关键字,再枚举第一关键字,然后我们惊奇的发现排序已经完成了,算法的复杂度是 \(\mathcal{O}\left(n\right)\) 的。

基数排序动图演示

对于每次排序的两个关键字,都可以从上一次的排序结果里获取。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int N=1e6+7;
char s[N];
int n,rk[N],tp[N],m;

int buc[N],sa[N],h[N];

void Qsort()
{
	for(int i = 0;i <= m;i ++) buc[i] = 0;
	for(int i = 1;i <= n;i ++) buc[rk[i]] ++;
	for(int i = 1;i <= m;i ++) buc[i] += buc[i-1];
	for(int i = n;i >= 1;i --) 
		sa[buc[rk[tp[i]]]--] = tp[i];
}

int main()
{
	scanf("%s",s+1);n = strlen(s+1);
	for(int i = 1;i <= n;i ++) rk[i] = s[i]-'a'+1,tp[i] = i;
	m = 26;Qsort();m = 0;
	for(int i = 1;i <= n;i ++) rk[sa[i]] = s[sa[i]] == s[sa[i-1]]?m:++m;
	for(int ws = 1,p = 0;m < n;m = p,ws <<= 1,p = 0) {
		for(int i = n-ws+1;i <= n;i ++) tp[++p] = i;
		for(int i = 1;i <= n;i ++) if(sa[i] > ws) tp[++p] = sa[i]-ws;
		Qsort();p = 0;swap(tp,rk);
		for(int i = 1;i <= n;i ++) {
			rk[sa[i]] = tp[sa[i]] == tp[sa[i-1]] && tp[sa[i]+ws] == tp[sa[i-1]+ws] ? p:++p;
		}
	}
	for(int i = 1;i <= n;i ++) printf("%d ",sa[i]);
	puts("");
	for(int i = 1,p = 0;i <= n;h[rk[i++]] = p) {
		for(p?--p:p;s[i + p] == s[sa[rk[i]-1] + p]; ++p);
	}
	for(int i = 2;i <= n;i ++) printf("%d ",h[i]);
	return 0;
}
posted @ 2021-02-15 23:45  nao-nao  阅读(62)  评论(0编辑  收藏  举报