后缀数组 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;
}