后缀数组

后缀数组

概念

后缀,对于一个长为 \(n\) 的字符串 \(s\)\(s[i,n]\) 即为 \(s\) 的一个后缀 \((i \in [1,n])\)

\(s\) 的所有后缀进行排序。考虑两个数组 \(sa_i\)\(rk_i\)\(sa_i\) 表示排名第 \(i\) 的后缀的编号为 \(sa_i\)\(rk_i\) 表示编号为 \(i\) 的后缀的排名。

举个例子:对于 \(s=\)aabaaab,对它的后缀进行排序:

  1. \(s[4,7]\)aaab
  2. \(s[5,7]\)aab
  3. \(s[1,7]\)aabaaab
  4. \(s[6,7]\)ab
  5. \(s[2,7]\)abaaab
  6. \(s[7,7]\)b
  7. \(s[3,7]\)baaab

则有:

\(i\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\) \(7\)
\(sa_i\) \(4\) \(5\) \(1\) \(6\) \(2\) \(7\) \(3\)
\(rk_i\) \(3\) \(5\) \(7\) \(1\) \(2\) \(4\) \(6\)

于是有性质:\(sa[rk[i]] = rk[sa[i]]\)

求解

倍增法求解

常用倍增法求解。

首先考虑这样一个问题:

考虑对于两个长度均为 \(2n\) 的字符串 \(s,t\) ,若 \(s>t\) 当且仅当:

  1. \(s[1,n]>t[1,n]\)
  2. \(s[1,n]=t[1,n]\)\(s[n + 1,2n]>t[n+1,2n]\)

于是,对于许多长度为 \(2n\) 的字符串排序,可以以每个字符串的前 \(n\) 位大小为第一关键字、后 \(n\) 位大小为第二关键字进行排序,即可以将每一个 \(s\) 的前 \(n\) 位和后 \(n\) 位作为一个二元组进行排序。

考虑运用类似的方式对于后缀进行排序。考虑倍增模型,从局部到全局逐层排序。

每一轮排序枚举 \(k \in[0,\lfloor\log_2 n\rfloor]\),对于每个 \(k\),将 \(rk_{sa_i}\) 作为第一关键字,\(rk_{sa_i+2^k}\) 作为第二关键字,对所有后缀进行这一轮的排序,其中 \(rk\)\(sa\) 是上一轮排序得出的数组。规定若 \(sa_i+2^k>n\),则 \(rk_{sa_i+2^k}=0\)。于是在进行 \(k\) 轮倍增后即可求出 \(sa\) 数组,时间复杂度 \(O(n \log ^2 n)\)

时间复杂度瓶颈在于 \(O(n \log n)\) 的排序。由于是二元组排序,考虑基数排序。

基数排序优化

基数排序

基数排序是一种排序。考虑对于 \(a_i\) 进行排序,考虑从低到高枚举每一位(个位、十位、百位……)即枚举 \(k(k \in\{1,2,\dots\})\),对于 \(a_i\) 由低至高的第 \(k\) 位进行桶排,即以本轮排序前 \(a_i\) 的大小为第二关键字,以第 \(k\) 位大小为第一关键字。如此进行 \(m=\log_{10}(\max a_i)\) 轮排序后,即可得到结果。另 \(V\) 为一次桶排的值域(若 \(a_i\) 为整数则 \(V\) 为 10),则时间复杂度为 \(O(nm+\sum V)\)

于是,对于每一轮排序,先对第二关键字即 \(rk_{sa_i+2^k}\) 进行桶排,再对第二关键字即 \(rk_{sa_i}\) 进行桶排,排序的时间复杂度从 \(O(n \log n)\) 降为 \(O(n)\)。总时间复杂度为 \(O(n \log n)\)

P3809 【模板】后缀排序

给出长为 \(n\) 的仅由英文大小写字母组成的字符串 \(s\),输出 \(s\)\(sa\) 数组。\(1 \le n \le 10^6\)

点击查看代码
const int N = 1e6 + 10;
int n;
char s[N];
int sa[N], lsa[N], rk[N << 1], lrk[N << 1], cnt[N];
// lsa 和 lrk 是拷贝的 sa 和 rk
// rk 开 2 倍是为了让超出 n 的 i+2^k 的 rk 值为 0
signed main() {
	cin >> (s + 1);
	n = strlen(s + 1);
	int m = 128;	// 初始值域要大于 'z' 的 ASCII 码
	for (int i = 1; i <= n; i++) rk[i] = s[i];
	for (int i = 1; i <= n; i++) cnt[rk[i]]++;
	for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
	for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;	// 对第一个字符进行桶排
	for (int k = 1; k < n; k <<= 1, m = n) {
        // 进行两次桶排实现对于二元组的基数排序
        // 对第二关键字进行桶排
		memset(cnt, 0, sizeof(cnt));
		for (int i = 1; i <= n; i++) lsa[i] = sa[i];
		for (int i = 1; i <= n; i++) cnt[rk[lsa[i] + k]]++;
		for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
		for (int i = n; i >= 1; i--) sa[cnt[rk[lsa[i] + k]]--] = lsa[i];
        // 对第一关键字进行桶排
		memset(cnt, 0, sizeof(cnt));
		for (int i = 1; i <= n; i++) lsa[i] = sa[i];
		for (int i = 1; i <= n; i++) cnt[rk[lsa[i]]]++;
		for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
		for (int i = n; i >= 1; i--) sa[cnt[rk[lsa[i]]]--] = lsa[i];
        // 用求得的 sa 数组求解 rk 数组
		for (int i = 1; i <= n; i++) lrk[i] = rk[i];
		for (int p = 0, i = 1; i <= n; i++) {
			if (lrk[sa[i]] == lrk[sa[i - 1]] && lrk[sa[i] + k] == lrk[sa[i - 1] + k]) {
				rk[sa[i]] = p;		// 若排序后两者完全相同,则排名也相同
			} else rk[sa[i]] = ++p;	// 否则排名不同
		}
	}
	for (int i = 1; i <= n; i++) 
		cout << sa[i] << " ";
	return 0;
}

常数优化

  1. 第二关键字无需桶排
    • 对于 \(sa+2^k\) 大于 \(n\)\(rk\) 必然为 \(0\) 就直接放到最前面。
    • 否则按 \(rk\) 从低到高枚举,如果当前的 \(sa_i>k\),说明它能与前面的字符串拼成长度为 \(2k\) 的字符串。由于是按照排名遍历,直接插入 \(sa_i-k\) 就是正确排序。
  2. 值域限制,每轮结束后将值域改为最大的 \(rk\) 即可,不必改成 \(n\)
  3. \(rk\) 的最大值等于 \(n\) 说明此时排名已经各不相同,直接退出即可。
点击查看代码
const int N = 1e6 + 10;
int n;
char s[N];
int sa[N], lsa[N], rk[N << 1], lrk[N << 1], cnt[N];
signed main() {
	cin >> (s + 1);
	n = strlen(s + 1);
	int m = 128;
	for (int i = 1; i <= n; i++) rk[i] = s[i];
	for (int i = 1; i <= n; i++) cnt[rk[i]]++;
	for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
	for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
	for (int k = 1, p = 0; k < n && p < n; k <<= 1, m = p) {
		int cur = 0;
		for (int i = n - k + 1; i <= n; i++) lsa[++cur] = i;
		for (int i = 1; i <= n; i++) 
			if (sa[i] > k) lsa[++cur] = sa[i] - k;
		memset(cnt, 0, sizeof(cnt));
		for (int i = 1; i <= n; i++) cnt[rk[lsa[i]]]++;
		for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
		for (int i = n; i >= 1; i--) sa[cnt[rk[lsa[i]]]--] = lsa[i];
		for (int i = 1; i <= n; i++) lrk[i] = rk[i];
		p = 0;
		for (int i = 1; i <= n; i++) {
			if (lrk[sa[i]] == lrk[sa[i - 1]] && lrk[sa[i] + k] == lrk[sa[i - 1] + k]) {
				rk[sa[i]] = p;
			} else rk[sa[i]] = ++p;
		}
	}
	for (int i = 1; i <= n; i++) 
		cout << sa[i] << " ";
	return 0;
}

应用

我们程两个字符串的最长公共前缀(LCP)为满足 \(s[1,x]=t[1,x]\) 的最大的 \(x\)

\(height\) 数组

定义 \(height_i=\text{LCP}(sa_i,sa_{i-1})\),即排名第 \(i\) 的后缀和它前一位的后缀的最长公共前缀。为方便书写记作 \(ht_i\),规定 \(ht_1=0\)

考虑求解。

考虑证明 \(ht_{rk_i} \geq ht_{rk_{i-1}}-1\)

\(ht_{rk_{i-1}} \le 1\) 时,右边小于等于 \(0\),上式显然成立。

否则,有 \(ht_{rk_i-1}=\text{LCP}(sa_{rk_{i-1}},sa_{rk_{i-1}-1})>1\),即后缀 \(i-1\) 与后缀 \(sa_{rk_{i-1}-1}\) 有长度为 \(ht_{rk_i-1}\) 的 LCP。考虑用 \(aA\) 表示这个 LCP,其中 \(a\) 为一个字符,\(A\) 为一个非空字符串,\(A\) 的长度为 \(ht_{rk_i-1}-1\)。那么后缀 \(i-1\) 可以表达为 \(aAB\),后缀 \(sa_{rk_{i-1}-1}\) 可以表示为 \(aAC\),其中 \(B>C\)\(C\) 可能为空,\(B\) 一定非空。

于是,后缀 \(i\) 可以表示为 \(AB\),且存在后缀 \(sa_{rk_{i-1}-1}+1\)\(AC\)。因为后缀 \(sa_{rk_i-1}\) 在大小排名上只比后缀 \(sa_{rk_i}\) 小一位,而 \(AB>AC\),则 \(AC \le\) 后缀 \(sa_{rk_i-1}< AB\)。此时显然后缀 \(i\)\(AB\) 与后缀 \(sa_{rk_i-1}\) 有共同前缀 \(A\)

可得 \(ht_{rk_i}=\text{LCP}(sa_{rk_i},sa_{rk_i-1})=\text{LCP}(i,sa_{rk_i-1}) \geq |A|=ht_{rk_{i-1}}-1\)。得证。

于是可以依此求出 \(height\) 数组。

点击查看代码
for (int k = 0, i = 1; i <= n; i++) {
    if (rk[i] == 0) continue;
    if (k) k--;	// k 就是 ht[rk[i-1]],于是有 ht[rk[i]] >= k-1
    while (s[i + k] == s[sa[rk[i] - 1] + k]) k++;	// 暴力扩展
    ht[rk[i]] = k;
} 

求解 LCP

考虑证明:\(\text{LCP}(sa_i,sa_j)=\min(\text{LCP}(sa_i,sa_k),\text{LCP}(sa_k,sa_j))(i \le k \le j)\)

考虑反证法。

假设 \(p=\min(\text{LCP}(sa_i,sa_k),\text{LCP}(sa_k,sa_j))\),则 \(\text{LCP}(i,k)\geq p,\text{LCP}(sa_k,sa_j) \geq p\)。设后缀 \(sa_i\)\(A\),后缀 \(sa_j\)\(B\),后缀 \(sa_k\)\(C\)。于是 \(A,C\)\(p\) 个字符相同,\(B,C\)\(p\) 个字符相同。所以 \(A,B\)\(p\) 个字符相同,即 \(\text{LCP}(sa_i,sa_j) \geq p\)

\(\text{LCP}(sa_i,sa_j)=q\)\(q>p\)。则 \(q \geq p+1\),且 \(A_{p+1}=B_{p+1}\)

由于 \(p=\min(\text{LCP}(sa_i,sa_k),\text{LCP}(sa_k,sa_j))\),所以 \(A_{p+1} \neq C_{p+1}\)\(B_{p+1} \neq C_{p+1}\)。于是 \(A_{p+1} \neq B_{p+1}\),矛盾。则假设不成立,则 \(q \le p\),即 \(\text{LCP}(sa_i,sa_j) \le p\)

所以 \(\text{LCP}(sa_i,sa_j)=p=\min(\text{LCP}(sa_i,sa_k),\text{LCP}(sa_k,sa_j))\),得证。

依此,易得:

\[\text{LCP}(sa_i,sa_j)=\min_{k=i+1}^j \text{LCP}(sa_k, sa_{k-1})=\min_{k=i+1}^j ht_k \]

至此 LCP 问题转化为求 \(ht\) 数组的区间最小值。可利用 ST 表求解。

点击查看代码
int sa[N], lsa[N], rk[N << 1], lrk[N << 1], cnt[N], ht[N];
il void SA(int n) {
	int m = 128;
	for (int i = 1; i <= n; i++) rk[i] = s[i];
	for (int i = 1; i <= n; i++) cnt[rk[i]]++;
	for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
	for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
	for (int k = 1, p = 0; k < n && p < n; k <<= 1, m = p) {
		int cur = 0;
		for (int i = n - k + 1; i <= n; i++) lsa[++cur] = i;
		for (int i = 1; i <= n; i++) 
			if (sa[i] > k) lsa[++cur] = sa[i] - k;
		memset(cnt, 0, sizeof(cnt));
		for (int i = 1; i <= n; i++) cnt[rk[lsa[i]]]++;
		for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
		for (int i = n; i >= 1; i--) sa[cnt[rk[lsa[i]]]--] = lsa[i];
		for (int i = 1; i <= n; i++) lrk[i] = rk[i];
		p = 0;
		for (int i = 1; i <= n; i++) {
			if (lrk[sa[i]] == lrk[sa[i - 1]] && lrk[sa[i] + k] == lrk[sa[i - 1] + k]) {
				rk[sa[i]] = p;
			} else rk[sa[i]] = ++p;
		}
	}
	for (int k = 0, i = 1; i <= n; i++) {
		if (rk[i] == 0) continue;
		if (k) k--;
		while (s[i + k] == s[sa[rk[i] - 1] + k]) k++;
		ht[rk[i]] = k;
	}
}
int mn[N][M], Log2[N];
il void init(int n) {
	for (int i = 2; i <= n; i++) Log2[i] = Log2[i / 2] + 1;
	for (int i = 1; i <= n; i++) mn[i][0] = ht[i];
	for (int j = 1; j <= Log2[n]; j++) 
		for (int i = 1; i + (1 << j) - 1 <= n; i++) 
			mn[i][j] = min(mn[i][j - 1], mn[i + (1 << (j - 1))][j - 1]);
}
il int getmn(int l, int r) {
	if (l > r) return INF;
	int s = Log2[r - l + 1];
	return min(mn[l][s], mn[r - (1 << s) + 1][s]);
}
il int get\text{LCP}(int x, int y) {
	if (x > y) swap(x, y);
	return getmn(x + 1, y);
}
posted @ 2026-07-03 08:11  Zctf1088  阅读(5)  评论(0)    收藏  举报