【笔记】后缀数组
后缀数组 (SA)
0 约定
字符串下标从 \(1\) 开始。
字符串 \(s\) 的长度为 \(n\)。
「后缀 \(i\)」:指从 \(i\) 开始的 \(s\) 的后缀,即 \(s[i\dots n]\)。
1 定义
这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。
后缀数组,\(sa[i]\) 表示将一个字符串的所有非空后缀按字典序排序后,从小到大的第 \(i\) 大的后缀的起点。就是问她 "第 \(i\) 个是哪个后缀?"。
排名数组,\(rk[i]\) 表示后缀 \(i\) 的排名(Warning 这里只是为后文叙述方便引入的辅助数组,也没有严格的名称)。就是问她 "第 \(i\) 个后缀排哪?"。
显然,这两个数组满足 \(sa[rk[i]]=rk[sa[i]]=i\)。
2 求解
2.1 思想
先谈更 Native 的倍增。她较线性的 DC3 优势在于实现容易,常数小,空间复杂度小,而时间复杂度也只多一个 \(\log\)。
字典序总是具有可以贪心的性质,从贪心入手,类比 ST 表,我们另记 \(rk^\prime[i][j]\) 表示从 \(i\) 开始往后的长度为 \(2^j+1\) 的字符串,即 \(S[i\dots i+2^j]\),排序后的位置。
然后,我们枚举 \(j\),考虑转移,对 \(rk^\prime[i][j]\) 她其实就等价于 \((rk^\prime[i][j-1],rk^\prime[i+2^{j-1}][j])\) 这个二元组,我们对这 \(n\) 个二元组排序,即可得到新的 \(rk^\prime\)。
2.2 实现
2.2.1 易错
- 注意 \(rk^\prime\) 的右端点是很可能超过 \(n\) 的,但是因为 \(s\) 后面都是
\0
,比所有的小写字母都小,所以没有影响。如果多测就要注意了。
2.2.2 过程
- 首先对
2.3 Code
/*
* Time Spent:
* Solution:
Tag:
* Summary:
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
char s[N];
int n, sa[N], rk[N], w[N], sw[N], x[N], y[N];
int main () {
#ifdef LOCAL
freopen("_1.in", "r", stdin);
freopen("_1.out", "w", stdout);
#else
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> s+1, n = strlen(s+1);
int m = max(n, 1000);
for (int i = 1; i <= n; i++) w[s[i]]++;
for (int i = 1; i <= m; i++) sw[i] = sw[i-1] + w[i];
for (int i = 1; i <= n; i++) x[sw[s[i]]--] = i;
for (int i = 1; i <= n; i++)
rk[x[i]] = rk[x[i-1]] + !(s[x[i]] == s[x[i-1]]);
for (int j = 1; j <= n; j <<= 1) {
memset(w, 0, sizeof w);
for (int i = 1; i <= n; i++) w[rk[i+j]]++;
sw[0] = w[0];
for (int i = 1; i <= m; i++) sw[i] = sw[i-1] + w[i];
for (int i = 1; i <= n; i++) x[sw[rk[i+j]]--] = i;
memset(w, 0, sizeof w);
for (int i = 1; i <= n; i++) w[rk[i]]++;
for (int i = 1; i <= m; i++) sw[i] = sw[i-1] + w[i];
for (int i = n; i; i--)
y[sw[rk[x[i]]]--] = x[i];
static int r[N];
memset(r, 0, sizeof r);
for (int i = 1; i <= n; i++)
r[y[i]] = r[y[i-1]] + !(rk[y[i]] == rk[y[i-1]] && rk[y[i]+j] == rk[y[i-1]+j]);
memcpy(rk, r, sizeof r);
}
for (int i = 1; i <= n; i++) sa[rk[i]] = i;
for (int i = 1; i <= n; i++) cout << sa[i] << ' ';
return 0;
}
/*
g++ _1.cpp -o _1 -O2 -std=c++14 -DLOCAL; ./_1.exe
*/
对于一个子串,出现它的后缀的排名是一段连续的区间。可以二分左右边界求得。