- 写在前面
- 前置小碎骨
- 计数排序
- 基数排序
- 一些约定
- 后缀数组
- 定义
- 倍增法构造
- 优化
- 代码及解释
- 再优化
- LCP 问题
- 一些定义
- 引理:LCP Lemma
- 引理:LCP Theorem
- 推论:LCP Corollary
- 引理
- 快速求 height
- 后缀树
- 暴力构建
- 虚树 + 后缀数组
- 前置知识
- 构建方法
- 套 SA
- 后缀自动机
- Ukkonen
- 后缀自动机
- 写在最后
绝对不咕
一篇就够了!
写在前面
会考虑整个与标题相关的二次创作。
什么时候有能力再说
前置小碎骨
计数排序
可以参考:OI-wiki 计数排序。
计数排序是一种与桶排序类似的排序方法。
将长度为 \(n\) 的数列 \(a\) 排序后放入 \(b\) 的代码如下,
其中 \(w\) 为值域,即 \(\max\{a_i\}\)。
int a[kMaxn], b[kMaxn], cnt[kMaxw];
for (int i = 1; i <= n; ++ i) ++ cnt[a[i]];
for (int i = 1; i <= w; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) b[cnt[a[i]] --] = a[i];
其中,在对 \(cnt\) 求前缀和后,
\(cnt_i\) 为小于 \(i\) 的数的数量,即为 \(i\) 的排名。
因此在下一步中,可以根据排名赋值。
复杂度为 \(O(n+w)\),值域与 \(n\) 同阶时复杂度比较优秀。
基数排序
这玩意比较水,参考 OI-wiki 基数排序。
个人认为基数排序只是一种思想,并不算一种排序方法。
它仅仅是将 k 个排序关键字分开依次考虑,实际每次排序还是靠计数排序实现。
请务必充分理解!
一些约定
- \(\mid \sum \mid\):字符集大小。
- \(S[i:j]\):由字符串 \(S\) 中 \(S_i\sim S_j\) 构成的子串。
- \(S_1
:字符串 \(S_1\) 的字典序 \( 。 - 后缀:从某个位置 \(i\) 开始,到串末尾结束的子串,后缀 \(i\) 等价于子串 \(S[i:n]\)。
后缀数组
网上部分题解直接开讲优化后面目全非的代码。
*这太野蛮了*
这里参考了 OI-wiki 上的讲解。
定义
字符串 \(S\) 的后缀数组 \(A\),被定义为一个数组,内容是其所有后缀 按字典序排序后的起始下标。
有: \(S[A_{i-1}:n] 成立。
举例:这里有一个可爱的字符串:\(S=\text{yuyuko}\)。
\(\text{k
具体地,有:
排名 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
下标 | \(5\) | \(6\) | \(4\) | \(2\) | \(3\) | \(1\) |
后缀 | \(\text{ko}\) | \(\text{o}\) | \(\text{uko}\) | \(\text{uyuko}\) | \(\text{yuko}\) | \(\text{yuyuko}\) |
显然不同后缀的排名必然不同(因为长度不等)
倍增法构造
先将所有 \(S[i:j]\) 进行排序。
每次通过 \(S[i:i+2^{k-1}-1]\) 的大小关系,求出 \(S[i:i+2^k-1]\) 的大小关系。
对于 \(S[i:i+2^k-1]\) 和 \(S[j:j+2^k-1]\),分别将它们裂开,分成两成长度为 \(i+2^{k-1}\) 的串。
设 \(A_i = S[i:i+2^{k-1}-1]\),\(B_i = S[i+2^{k-1}:i+2^k-1]\)。
考虑字典序排序的过程,则 \(S[i:i+2^k-1] 的条件为:
考虑每一次倍增时,都使用 sort 按双关键字 \(A_i\) 和 \(B_i\) 进行排序。
时间复杂度显然为 \(O(n\log^2 n)\)
优化
sort 太慢啦!
发现后缀数组值域即为 \(n\),又是多关键字排序。
考虑基数排序。
上面已经给出一个用于比较的式子:
\(A_i,B_i\) 大小关系已知,直接基数排序实现即可。
先将 \(B_i\) 作为第二关键字排序。
再将 \(A_i\) 作为第一关键字排序。
单次计数排序复杂度 \(O(n + w)\)(\(w\) 为值域,最大与 \(n\) 同阶)。
排序变为 \(O(n)\) 级别,时间复杂度 \(O(n\log n)\)。
代码及解释
P3809 【模板】后缀排序
这是一份没有优化过的代码,是对上述过程的直接实现。
只能获得 73 分。
可发现代码的空间复杂度为 \(O(n)\)。
代码实现较为复杂,下面会进行详细讲解。
//知识点:SA
/*
By:Luckyblock
I love Marisa;
But Marisa has died;
*/
#include
#include
#include
#include
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
char S[kMaxn];
//sa[i]: 倍增过程中子串[i:i+2^k-1]的排名,
//rk[i] 排名为i的子串 [i:i+2^k-1],
//它们互为反函数。
//rk 和 oldrk 要开2倍空间,下面会提到原因。
int n, m, sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1];
int id[kMaxn], cnt[kMaxn]; //用于计数排序的两个tmp数组
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
//=============================================================
int main() {
scanf("%s", S + 1);
n = strlen(S + 1);
m = std :: max(n, 300); //值域大小
//初始化 rk 和 sa
for (int i = 1; i <= n; ++ i) ++ cnt[(rk[i] = S[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;
//倍增过程。
//w = 2^{k-1},是已经推出的子串长度。
//注意此处的 sa 数组存的并不是后缀的排名,
//存的是指定长度子串的排名。
for (int w = 1; w < n; w <<= 1) {
//按照后半截 rk[i+w] 作为第二关键字排序。
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) id[i] = sa[i];
for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i] + w]]; //这里有越界风险,因此开了2倍空间,否则会被卡
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i] + w]] --] = id[i];
//按照前半截 rk[i] 作为第一关键字排序。
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) id[i] = sa[i];
for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i]]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i]]] --] = id[i];
//更新 rk 数组。
//这里可以滚动数组一下,但是可读性会比较差(
//卡常可以写一下。
for (int i = 1; i <= n; ++ i) oldrk[i] = rk[i];
for (int p = 0, i = 1; i <= n; ++ i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] && //判断两个子串是否相等。
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) { //这里有越界风险,因此开了2倍空间,否则会被卡
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++ p;
}
}
}
for (int i = 1; i <= n; ++ i) printf("%d ", sa[i]);
return 0;
}
这里定义了两个数组:
\(sa_i\):倍增中 排名为 \(i\) 的长度为 \(2^{k-1}\) 的子串。
\(rk_i\):倍增过程中子串 \(S[i:i+2^k-1]\) 的排名,
显然它们互为反函数,\(sa_{rk_i}=rk_{sa_i} = i\)。
首先初始化 \(rk\) 和 \(sa\)。
for (int i = 1; i <= n; ++ i) ++ cnt[(rk[i] = S[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;
初始化 \(rk_i = S_i\),即 \(S_i\) 的 \(\text{ASCII}\) 值。
虽然这样不满足值域在 \([1,n]\) 内,但体现了大小关系,可用于更新。 \(rk\) 的值之后还会更新。
子串长度为 \(1\),直接根据 \(rk_i\) 计数排序 \(sa\) 即可。
之后进入倍增。
每次倍增先后按照 后半截,前半截的 \(rk\) 作为关键字排序来更新 \(sa\)。
\(id\) 是一个 tmp 数组,存排序前的的 \(sa\)。
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) id[i] = sa[i];
for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i] + w]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i] + w]] --] = id[i];
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) id[i] = sa[i];
for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i]]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i]]] --] = id[i];
排后半截时 会枚举到 \(id[i]+w > n\) 怎么办?
考虑实际意义,出现此情况,表示该子串后半截为空。
空串字典序最小,考虑直接把 \(rk\) 开成两倍空间,则 \(rk[i]=0(i>n)\) 恒成立。
防止了越界,也处理了空串的字典序。
更新 \(rk\) 数组。
for (int i = 1; i <= n; ++ i) oldrk[i] = rk[i];
for (int p = 0, i = 1; i <= n; ++ i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++ p;
}
}
\(sa\) 为 \(rk\) 的反函数。
这里相当于根据有序的 \(sa\),离散化并去重 \(rk\)。
考虑两个子串 \(rk\) 相等的条件。
显然,当其前后两半均相等时,两子串相同,其 \(rk\) 才相同,则有上述的判断。
这里也会出现空串的情况,注意 2 倍空间。
再优化
被卡常了,排两次计数排序太慢啦!
观察对后半截排序时的特殊性质:
考虑更新前的 \(sa_i\) 的含义:排名为 \(i\) 的长度为 \(2^{k-1}\) 的子串。
在本次排序中,\(sa_i\) 是长度为 \(2^k\) 的子串 \(sa_{i}-2^{k-1}\) 的后半截。 \(sa_i\) 的排名将作为排序的关键字。
\(sa_i\) 的排名为 \(i\),则排序后 \(sa_{i}-2^{k-1}\) 的排名必为 \(i\)。
考虑直接赋值,那么第一次计数排序就可以写成这样:
int p = 0;
for (int i = n; i > n - w; -- i) id[++ p] = i; //后半截为空的串
for (int i = 1; i <= n; ++ i) { //根据后半截,直接推整个串的排名
if (sa[i] > w) id[++ p] = sa[i] - w;
}
注意后半截为空串的情况,这样的串排名相同且最小。
以及一些奇怪的常数优化:
减小值域。
发现值域大小 \(m\) 与计数排序复杂度有关。
其最小值应为 \(rk\) 的最大值,在更新 \(rk\) 时将其更新即可。
减少数组嵌套的使用,从而减少不连续内存访问。
在第二次计数排序时,将 \(rk_{id_i}\) 存下来。
用 cmp 函数判断两个子串是否相同。
同样是减少不连续内存访问,详见代码。
最终代码
//知识点:SA
/*
By:Luckyblock
I love Marisa;
But Marisa has died;
*/
#include
#include
#include
#include
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
char S[kMaxn];
int n, m, sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1];
int id[kMaxn], cnt[kMaxn], rkid[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
bool cmp(int x, int y, int w) { //判断两个子串是否相等。
return oldrk[x] == oldrk[y] &&
oldrk[x + w] == oldrk[y + w];
}
//=============================================================
int main() {
scanf("%s", S + 1);
n = strlen(S + 1);
m = std :: max(n, 300); //值域大小
//初始化 sa数组
for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[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;
//倍增过程。
//此处 w = 2^{k-1},是已经推出的子串长度。
//注意此处的 sa 数组存的并不是后缀的排名,
//存的是指定长度子串的排名。
for (int p, w = 1; w < n; w <<= 1) {
//按照后半截 rk[i+w] 作为第二关键字排序。
p = 0;
for (int i = n; i > n - w; -- i) id[++ p] = i; //后半截为空的串
for (int i = 1; i <= n; ++ i) { //根据后半截,直接推整个串的排名
if (sa[i] > w) id[++ p] = sa[i] - w;
}
//按照前半截 rk[i] 作为第一关键字排序。
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) ++ cnt[(rkid[i] = rk[id[i]])];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rkid[i]] --] = id[i];
//更新 rk 数组。
//这里可以滚动数组一下,但是可读性会比较差(
//卡常可以写一下。
std ::swap(rk, oldrk);
m = 0; //直接更新值域 m
for (int i = 1; i <= n; ++ i) {
rk[sa[i]] = (m += (cmp(sa[i], sa[i - 1], w) ^ 1));
}
}
for (int i = 1; i <= n; ++ i) printf("%d ", sa[i]);
return 0;
}
LCP 问题
感谢论文哥!后缀数组-许智磊
\(\operatorname{lcp}(S,T)\) 定义为字符串 \(S\) 和 \(T\) 的最长公共前缀 (Longest common prefix),
即为最大的 \(l\le \min\{\mid S\mid,\mid T\mid\}\),满足 \(S_i=T_i(1\le i\le l)\)。
在许多后缀数组相关问题中,都需要它的帮助。
下文以 \(\operatorname{lcp}(i,j)\) 表示后缀 \(i\),\(j\) 的最长公共前缀。
下文会延续后缀数组中一些概念:
\(sa_i\):排名为 \(i\) 的后缀。
\(rk_i\):后缀 \(i\) 的排名。
一些定义
定义一些新的概念:
\(\operatorname{height}_i\) 表示 \(sa\) 中相邻两后缀 \(i-1\) 和 \(i\) 的 最长公共前缀。
\(h_i\) 表示后缀 \(i\),和 \(sa\) 中排名在 \(i\) 之前一位的后缀的 最长公共前缀。
由 \(rk_{sa_i} = i\),显然有 \(h_i = h_{rk_{sa_i}}=\operatorname{height}_{sa_i}\)。
引理:LCP Lemma
此引理是证明其他引理的基础。
证明,设 \(p = \min\{\operatorname{lcp}(i,j), \operatorname{lcp}(j,k)\}\),则有:
则 \(sa_i[1:p] = sa_j[1:p] = sa_k[1:p]\),可得 \(\operatorname{lcp}(i,k)\ge p\)。
再考虑反证法,设 \(\operatorname{lcp}(i,k) =q > p\)。
则 \(sa_i[1:q]=sa_k[1:q]\),则有 \(sa_i[p+1]=sa_k[p+1]\)
对 \(p\) 的取值分类讨论:
- \(p=\operatorname{lcp}(i,j) < \operatorname{lcp}(j,k)\)。
则有 \(sa_i[p+1] < sa_j[p+1] = sa_k[p+1]\)。 - \(p=\operatorname{lcp}(j,k) < \operatorname{lcp}(i,j)\)。
则有 \(sa_i[p+1] = sa_j[p+1] < sa_k[p+1]\)。 - \(p=\operatorname{lcp}(j,k) = \operatorname{lcp}(i,j)\)。
则有 \(sa_i[p+1] < sa_j[p+1] < sa_k[p+1]\)。
\(sa_i[p+1]
结合 \(\operatorname{lcp}(i,p)\ge p\),得证原结论成立。
引理:LCP Theorem
由 LCP Lemma,可知显然成立。
根据这个优美的式子,求解任意两个后缀的 \(\operatorname{lcp}\) 变为求解 \(\operatorname{height}\) 的区间最值问题。
可通过 st 表 实现 \(O(n\log n)\) 预处理,\(O(1)\) 查询。
问题转化为如何快速求 \(\operatorname{height}\)。
推论:LCP Corollary
排名不相邻的两个后缀的 \(\operatorname{lcp}\) 不超过它们之间任何相邻元素的 \(\operatorname{lcp}\)。
证明由引理 LCP Lemma 显然可得。
但是涛哥钦定我写一下证明,那我就不胜惶恐地写了(
类似 LCP Lemma,考虑反证法。
设 \(\operatorname{lcp}(sa_i,sa_j)< \operatorname{lcp}(sa_i, sa_k)\),则有下图:
考虑字典序比较的过程。
若 \(sa_i < sa_j\),则有 \(sa_i[{\operatorname{lcp}(sa_i,sa_j)+1}]
即图中的字符 \(x
此时考虑比较 \(sa_j\) 与 \(sa_k\) 的字典序。
由图,\(\operatorname{lcp}(sa_j,sa_k) = \operatorname{lcp}(sa_i,sa_j)\)。
而 \(\operatorname{lcp}(sa_i,sa_k) > \operatorname{lcp}(sa_i,sa_j)\),则 \(sa_k[{\operatorname{lcp}(sa_j,sa_k)+1}] = x\)。
又 \(x
与已知矛盾,反证原结论成立。
引理
用来快速计算 \(\operatorname{height}\)。
个人喜欢叫它不完全单调性。
证明考虑数学归纳。
当 \(h_{i-1}\le 1\) 时,结论显然成立,因为 \(h_i \ge 0\)。
当 \(h_{i-1}>1\) 时:
设 \(u = i, \, v = sa_{rk_i-1}\),有 \(h_i = \operatorname{lcp}(u,v)\)。
\(sa\) 中 \(v\) 在 \(u\) 前一位置。
设 \(u' = i-1, \, v' = sa_{rk_{i-1}-1}\),有 \(h_{i-1} = \operatorname{lcp}(u',v')\)。
\(sa\) 中 \(v'\) 在 \(u'\) 前一位置。
由 \(h_{i-1} = \operatorname{lcp}(u',v')>1\),则 \(u',v'\) 必有公共前缀。
考虑删去 \(u',v'\) 的第一个字符,设其分别变成 \(x,y\)。
显然 \(\operatorname{lcp}(x,y) = h_{i-1}-1\),且仍满足字典序 \(y
\(u' = i-1\),则删去第一个字符后,\(x\) 等于后缀 \(i\)。
则 \(sa\) 中,有 \(y
又 \(sa\) 中,\(v\) 在 \(u\) 前一位置,则有 \(y
根据 LCP Corollary,有:
得证。
快速求 height
由 定义 \(h_i = \operatorname{height}_{sa_i}\),只需快速求出 \(h\),便可 \(O(n)\) 复杂度获得 \(\operatorname{height}\)。
由引理已知 \(\forall 1\le i\le n,\, h_i\ge h_{i-1}-1\)。
\(h_i=\operatorname{lcp}(i, sa_{rk_i -1})\) 具有不完全单调性,考虑正序枚举 \(i\) 进行递推。
当 \(rk_i=1\) 时, \(sa_{rk_i-1}\) 不存在,特判 \(h_i=0\)。
当 \(i=1\),暴力比较出 \(\operatorname{lcp}(i,sa_{rk_i-1})\),比较次数 \(
若上述情况均不满足,由引理知,\(h_i=\operatorname{lcp}(i,sa_{rk_i-1})\ge h_{i-1}-1\),两后缀前 \(h_{i-1}-1\) 位相同。
可从第 \(h_{i-1}\) 位开始比较两后缀计算出 \(h_i\),比较次数 \(=h_i-h_{i-1}+2\)
计算出 \(h_i\),可直接得到 \(\operatorname{height}_{sa_i}\)。
代码中并没有专门开 \(h\) 数组,其中\(h_i = k\):
void GetHeight() {
for (int i = 1, k = 0; i <= n; ++ i) {
if (rk[i] == 1) k = 0;
else {
if (k > 0) k --;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n &&
S[i + k] == S[j + k]) {
++ k;
}
}
height[rk[i]] = k;
}
}
复杂度分析:
\(k\le n\),最多减 \(n\) 次,则最多会在比较中加 \(2n\) 次。
则总复杂度为 \(O(n)\)。
后缀树
定义:一个字符串 \(S\) 的所有后缀 \(S[i:n]\,(1\le i\le n)\) 组成的 Trie 树。
构建后缀树 准备学习 SAM 后用 SAM 建后缀树。
暂时没有写虚树 + 后缀数组法的代码。
如果以后有必要会去写一下。
暴力构建
考虑增量法。
暴力枚举原串的每个后缀,将其插入字典树。
本质不同的子串个数最多达到 \(O(n^2)\) 级别,故节点数为 \(O(n^2)\)。
此时使用后缀树与直接枚举原串的子串等价,复杂度 \(O(n^2)\)。
虚树 + 后缀数组
虽然节点数为 \(O(n^2)\),但叶节点数仅有 \(O(n)\) 个。
大部分节点有且仅有一个孩子。
考虑建后缀树的虚树,将树中的链缩成一条边。
前置知识
SA + OI-wiki-虚树。
简单介绍虚树(抄一波课件):
对于树 \(T=(V,E)\),给定关键点集 \(S\subseteq V\),则可定义虚树 \(T'=(V',E')\)。
对于点集 \(V'\subseteq V\),使得 \(u\in V'\) 当且仅当 \(u\in S\),或 \(\exist x,y\in S,\operatorname{lca}(x,y)=u\)。
对于边集,\((u,v)\in E'\),当且仅当 \(u,v\in V'\),且 \(u\) 为 \(v\) 在 \(V'\) 中深度最浅的祖先。
个人理解:
仅保留关键点及其 lca,缩子树成边,仅保留分叉节点。
可能 删去一些不包含关键点的子树。
压缩了树的信息,同时丢失了部分树的信息。
构建方法
假设已知后缀树的结构。
考虑增量法,每次向虚树中添加一个关键点
显然,一棵后缀树的关键点 即其 \(n\) 个叶节点。
先求得关键节点的 dfs 序,规定按字典序 dfs。
单调栈维护虚树最右侧的链(上一个关键点与根的链),栈顶一定为上一个关键点。
单调栈中节点深度递增。
每加入一个关键点 \(a_i\),令 \(\operatorname{lca}(a_{i-1},a_i)=w\)。
将栈顶 \(dep_x > dep_w\) 的弹栈,加入 \(w,a_i\),即为新的右链。
若栈顶存在 \(dep_x=dep_w\),不加入 \(w\) 节点。
在此过程中维护每个节点的父节点,在弹栈时进行连边并维护信息,即得虚树。
总复杂度 \(O(n\log n)\) 级别。
套 SA
但是后缀树的结构并不已知。 已知还建虚树干什么
发现上述过程中并没有用到后缀的性质。
总结一下上述建虚树的过程:
- 求关键点的 dfs 序。
- 单调栈维护右链。
- 插入关键节点,求两相邻关键点的 \(\operatorname{lca}\),比较深度。
关键点 \(i\) 的 dfs 序即为后缀数组中的 \(sa_i\),可 \(O(n)\) 求得。
单调栈的复杂度为 \(O(n)\)。
两关键节点 代表排名相邻的 两后缀。
插入 \(sa_i\) 时,\(1\sim \operatorname{lca}\) 的链 为两后缀的最长公共前缀。
则 \(\operatorname{lca}\) 节点代表 \(\operatorname{lcp}(sa_{i-1}, sa_i)\)。
\(\operatorname{lcp}\) 的长度,即 \(\operatorname{lca}\) 的深度,等于 \(\operatorname{height}_i\)。
\(\operatorname{lca}\) 节点是谁不知道,但这并不妨碍弹栈。弹栈只关心节点的深度。
若栈顶存在 \(dep_x=\operatorname{lcp}\),则 \(lca\) 已在栈中。
否则新建一个 \(dep_x=\operatorname{lcp}\) 的节点插入,当做 \(\operatorname{lca}\) 即可。
求 \(\operatorname{height}\) 的复杂度为 \(O(n \log n)\)。
则算法总复杂度为 \(O(n \log n)\)。
后缀自动机
定理
SAM 的 parent 树为反串后缀树。
因此可以使用 SAM 进行构建,时间复杂度为 \(O(n\mid \sum\mid)\) 或 \(O(n\log n)\)。
Ukkonen
Udk
复杂度 \(O(n)\),可直接构建出后缀树。
神仙算法,我直接跑路,建议百度搜索谷歌搜索学习。
后缀自动机
涛哥钦定我写一篇博客。
那我就先咕了
写在最后
参考资料:
OI-wiki SA
后缀数组详解 - 自为风月马前卒
「后缀排序SA」学习笔记 - Rainy7
后缀数组-许智磊
后缀数组学习笔记 _ Menci's Blog
OI-wiki 虚树
利用后缀数组构造后缀树_AZUI
写句子。
I want to be a rap...
犹豫了一下。
raper。
身 败 名 裂
魂音泉 喜欢。
Drinking Drinking more...
Eating Eating more...
FELT 喜欢。