[字符串学习笔记] 6. 后缀数组
6.1. 约定
本文中,下标从 \(0\) 开始计算,排名从 \(1\) 开始计算。当下标超出数组范围时,默认值为 \(0\)。
将后缀 \(s[i \ldots |s| - 1]\) 称为后缀 \(i\)。
将两个后缀 \(i, j\) 的 最长公共前缀 长度简记为 \(f(i, j)\)。
6.2. 定义
- 定义 排名数组 \(\mathit{rk}[i]\) 为 \(s[i \ldots |s| - 1]\) 在 \(s\) 的所有后缀中按照 字典序 的排名。\(0 \leq i \leq |s| - 1\) 且 \(\mathit{rk}\) 为 \(1 \sim |s|\) 的排列,两两不同。
- 定义 后缀数组 \(\mathit{sa}[i]\) 为排名第 \(i\) 的后缀在 \(s\) 中的起始位置。\(1 \leq i \leq |s|\)。
不难发现,\(\forall i \in [0, |s| - 1] \cap \mathbb Z\),有 \(\mathit{sa}[\mathit{rk}[i]] = i\);而 \(\forall i \in [1, |s|] \cap \mathbb Z\),有 \(\mathit{rk}[\mathit{sa}[i]] = i\)。
例如,当 \(s = \texttt{abaaab}\) 时,将所有后缀排好序:
即,
- \(\mathit{sa} = [2, 3, 4, 0, 5, 1]\)(下标从 \(1\) 开始)。
- \(\mathit{rk} = [4, 6, 1, 2, 3, 5]\)(下标从 \(0\) 开始)。
6.3. 实现
6.3.1. 朴素算法
存储所有后缀并进行 快速排序。比较字符串时间复杂度 \(\Theta(|s|)\),故总时间复杂度 \(\Theta({|s|}^2 \log |s|)\)。
6.3.2. 倍增算法
如果要比较两个长度相等的字符串 \(s, t\),可以先将字符串各分成两部分 \((s_1, s_2), (t_1, t_2)\) 且 \(|s_i| = |t_i|\)。将 \(s_1, t_1\) 的比较结果作为第一关键字,\(s_2, t_2\) 的比较结果作为第二关键字,就可以比较出 \(s, t\)。
根据这种思路进行 倍增。
首先,将 \(s\) 中的字符排序并给予排名 \(\mathit{rk}_1\)。注意此处 \(\mathit{rk}_1\) 不一定 两两不同。
对于 \(0 \leq i \leq |s| - 1\),将 \((\mathit{rk}_1[i], \mathit{rk}_1[i + 1])\) 二元组排序并给予排名 \(\mathit{rk}_2\)。\(\mathit{rk}_2[i]\) 代表着 \(s[i \ldots \min(i + 1, |s| - 1)]\) 的排名。
对于 \(0 \leq i \leq |s| - 1\),将 \((\mathit{rk}_2[i], \mathit{rk}_2[i + 2])\) 二元组排序并给予排名 \(\mathit{rk}_4\)。\(\mathit{rk}_4[i]\) 代表着 \(s[i \ldots \min(i + 3, |s| - 1)]\) 的排名。
同理,倍增地将 \((\mathit{rk}_w[i], \mathit{rk}_w[i + w])\) 二元组排序并给予排名 \(\mathit{rk}_{2w}\)。\(\mathit{rk}_{2w}\) 代表着 \(s[i \ldots \min(i + 2w - 1, |s| - 1)]\) 的排名。
当 \(w \geq |s|\) 时,得到的 \(\mathit{rk}_w\) 即最终的 \(\mathit{rk}\) 数组。另外,若 \(\mathit{rk}_w\) 已然两两不同,再倍增下去显然 无法影响结果,可以直接退出倍增。
时间复杂度 \(\Theta(n \log^2 n)\)。
代码
void SA(string s) {
for (int i = 0; i < s.size(); i++)
rk[i] = s[i], sa[i + 1] = i;
auto R = [&](int *p, int ind) {
return ind >= s.size() ? 0 : p[ind];
}; // 防止越界,当然也可开双倍空间解决
for (int w = 1; w < s.size(); w <<= 1) {
sort(sa + 1, sa + s.size() + 1, [&](int i, int j) {
return make_pair(R(rk, i), R(rk, i + w)) < make_pair(R(rk, j), R(rk, j + w));
});
copy(rk, rk + s.size(), ork);
int top = 0;
for (int i = 1; i <= s.size(); i++)
if (i > 1 && R(ork, sa[i]) == R(ork, sa[i - 1]) && R(ork, sa[i] + w) == R(ork, sa[i - 1] + w))
rk[sa[i]] = top;
else
rk[sa[i]] = ++top;
if (top == n) break; // 互不相同,停止倍增
}
}
6.3.3. 基数排序
由于双关键字排序的值域为 \(\Theta(n)\),故可以采用 基数排序 优化到 \(\Theta(n \log n)\)。
小优化:在首先对第二关键字进行排序时,可以先将 \(\mathit{sa}[i] + w \geq n\)(超出数组范围即对应的 \(\mathit{rk}\) 值为 \(0\) 且 最小)的 \(\mathit{sa}[i]\) 放到代码中 \(\mathit{id}\) 数组头部。
代码
void SA(string s) {
int m = 128; // rk 的值域
for (int i = 0; i < s.size(); i++)
rk[i] = s[i], buc[rk[i]]++;
for (int i = 1; i <= m; i++)
buc[i] += buc[i - 1];
for (int i = s.size() - 1; i >= 0; i--)
sa[buc[rk[i]]--] = i;
auto R = [&](int *p, int ind) {
return ind >= s.size() ? 0 : p[ind];
};
for (int w = 1, top; w < s.size(); w <<= 1) {
top = 0;
for (int i = s.size() - w; i < s.size(); i++)
id[++top] = i; // 顺序无关紧要
for (int i = 1; i <= s.size(); i++)
if (sa[i] >= w)
id[++top] = sa[i] - w;
fill(buc + 1, buc + m + 1, 0);
for (int i = 0; i < s.size(); i++)
buc[rk[i]]++;
for (int i = 1; i <= m; i++)
buc[i] += buc[i - 1];
for (int i = s.size(); i >= 1; i--)
sa[buc[rk[id[i]]]--] = id[i];
copy(rk, rk + s.size(), ork);
top = 0;
for (int i = 1; i <= s.size(); i++)
if (i > 1 && R(ork, sa[i]) == R(ork, sa[i - 1]) && R(ork, sa[i] + w) == R(ork, sa[i - 1] + w))
rk[sa[i]] = top;
else
rk[sa[i]] = ++top;
if (top == s.size()) break;
m = top; // 更新值域
}
}
6.4. Height 数组
6.4.2. 定义
定义 Height 数组 \(\mathit{ht}[i]\) 为排名第 \(i - 1\) 与第 \(i\) 的后缀的 最长公共前缀 长度,即 \(f(\mathit{sa}[i - 1], \mathit{sa}[i])\)。
6.4.3. 实现
引理:\(\forall i \in [2, |s|] \cap \mathbb Z\),\(\mathit{ht}[\mathit{rk}[i]] \geq \mathit{ht}[\mathit{rk}[i - 1]] - 1\)。
证明
当 \(\mathit{ht}[\mathit{rk}[i - 1]] \leq 1\) 时,\(\mathit{ht}[\mathit{rk}[i - 1]] - 1 \leq 0\),不等式显然成立。
当 \(\mathit{ht}[\mathit{rk}[i - 1]] > 1\) 时,代入得
\[f(\mathit{sa}[\mathit{rk}[i - 1]], \mathit{sa}[\mathit{rk}[i - 1] - 1]) = f(i - 1, \mathit{sa}[\mathit{rk}[i - 1] - 1]) > 1 \]将这两个后缀的 最长公共前缀 表示为 \(c + S\),其中 \(c\) 为字符,\(S\) 为长度为 \(\mathit{ht}[\mathit{rk}[i - 1]] - 1\) 的字符串。此时,后缀 \(i - 1\) 可以表示为 \(c + S + U\),而后缀 \(\mathit{sa}[\mathit{rk}[i - 1] - 1]\) 可以表示为 \(c + S + V\),且有 \(U < V\)。
删去首字符 \(c\),得到后缀 \(i = S + U\),后缀 \(\mathit{sa}[\mathit{rk}[i - 1] - 1] + 1 = S + V\)。因为后缀 \(\mathit{sa}[\mathit{rk}[i] - 1]\) 仅次于 后缀 \(i\),又有 \(S + U < S + V\),则 \(S + U \leq\) 后缀 \(\mathit{sa}[\mathit{rk}[i] - 1] < S + V\)。
显然,\(S\) 为后缀 \(i\) 与后缀 \(\mathit{sa}[\mathit{rk}[i] - 1]\) 的 公共前缀。则 \(f(i, \mathit{sa}[\mathit{rk}[i] - 1]) = \mathit{ht}[i] \geq |S| = \mathit{ht}[\mathit{rk}[i - 1]] - 1\)。\(\square\)
根据上述引理,直接求即可。代码十分好写。
void SA_height(string s) {
SA(s);
for (int i = 0; i < s.size(); i++) {
if (rk[i] == 1) continue;
ht[rk[i]] = i == 0 ? 0 : max(0, ht[rk[i - 1]] - 1);
while (i + ht[rk[i]] < s.size() && sa[rk[i] - 1] + ht[rk[i]] < s.size() && s[i + ht[rk[i]]] == s[sa[rk[i] - 1] + ht[rk[i]]]) // 需要判断是否越界
ht[rk[i]]++;
}
}
例题 SP32577 ADAPHOTO - Ada and Terramorphing
题解
引理:\(\forall i \in [3, |s|]\),\(f(\mathit{sa}[i], \mathit{sa}[j]) \leq \mathit{ht}[i] ~ (j < i - 1)\)。
证明
若下标超出字符串范围,访问到的字符值为 \(-\infty\)。
从 \(0\) 到 \(\mathit{ht}[i] - 1\) 正序遍历 \(k\)。若 \(s[\mathit{sa}[j] + k] < s[\mathit{sa}[i] + k]\),\(f(\mathit{sa}[i], \mathit{sa}[j]) = k < \mathit{ht}[i]\)。
如果 \(s[\mathit{sa}[j] \ldots \mathit{sa}[j] + \mathit{ht}[i] - 1] = s[\mathit{sa}[i] \ldots \mathit{sa}[i] + \mathit{ht}[i] - 1]\),则
\[s[\mathit{sa}[j] + \mathit{ht}[i]] \leq s[\mathit{sa}[i - 1] + \mathit{ht}[i]] < s[\mathit{sa}[i] + \mathit{ht}[i]] \]故 \(f(\mathit{sa}[i], \mathit{sa}[j]) = \mathit{ht}[i]\)。综上,\(f(\mathit{sa}[i], \mathit{sa}[j]) \leq \mathit{ht}[i]\),得证。
接下来就不难做了。构造出字符串 \(p \gets s + \texttt \# + t\)(\(\texttt \#\) 是 \(s, t\) 字符集中未出现的 分隔符)。根据上述引理可知,最长公共子串一定是 \(p\) 排名相差 \(1\) 的后缀的最长公共前缀。如果 \(\mathit{sa}[i - 1], \mathit{sa}[i]\) 有一个小于 \(|s|\),而另一个大于 \(|s|\),说明这两个后缀一个位于 \(s\),一个位于 \(t\),符合题意,就统计答案。
时间复杂度瓶颈在于求 SA 的 \(\Theta(n \log n)\)。参考代码:
int res;
string s, t;
signed main() {
cin >> s >> t;
string p = s + '#' + t;
SA_height(p);
for (int i = 2; i <= p.size(); i++)
if (sa[i - 1] < s.size() && sa[i] > s.size() || sa[i - 1] > s.size() && sa[i] < s.size())
res = max(res, ht[i]);
cout << res;
}
6.5. LCP 理论
6.5.1. LCP 引理
LCP 引理:
证明
简记 \(l\) 为 \(\min(f(\mathit{sa}[i], \mathit{sa}[j]), f(\mathit{sa}[j], \mathit{sa}[k]))\)。易知 \(f(\mathit{sa}[i], \mathit{sa}[j]), f(\mathit{sa}[j], \mathit{sa}[k])\) 均 \(\geq l\)。
因此,
\[{\color{#9f3} s[\mathit{sa}[i] \ldots \mathit{sa}[i] + l - 1]} = s[\mathit{sa}[j] \ldots \mathit{sa}[j] + l - 1] = {\color{#9f3} s[\mathit{sa}[k] \ldots \mathit{sa}[k] + l - 1]} \]可得 \(f(\mathit{sa}[i], \mathit{sa}[k]) \geq l\)。不妨假设 \(f(\mathit{sa}[i], \mathit{sa}[k]) \geq l + 1\)。
考虑 \(l\) 的定义,必然满足
- \(s[\mathit{sa}[i] + l] < s[\mathit{sa}[j] + l]\)。
- \(s[\mathit{sa}[j] + l] < s[\mathit{sa}[k] + l]\)。
二者中 至少一项。无论如何,\(s[\mathit{sa}[i] + l] < s[\mathit{sa[k]} + l]\),与假设相矛盾。故 \(f(\mathit{sa}[i], \mathit{sa}[k]) = l\)。\(\square\)
6.5.2. LCP 定理
LCP 定理:
证明
结合 LCP 引理易证。
\[\begin{aligned} f(\mathit{sa}[i], \mathit{sa}[j]) &= \min(f(\mathit{sa}[i], \mathit{sa}[i + 1]), f(\mathit{sa}[i + 1], \mathit{sa}[j])) \\ &= \min(f(\mathit{sa}[i], \mathit{sa}[i + 1]), f(\mathit{sa}[i + 1], \mathit{sa}[i + 2]), f(\mathit{sa}[i + 2], \mathit{sa}[j])) \\ &= \ldots \\ &= \min_{i - 1 \leq k \leq j} f(\mathit{sa}[k - 1], \mathit{sa}[k]) \\ &= \min_{i - 1 \leq k \leq j} \mathit{ht}[k] \end{aligned} \]

浙公网安备 33010602011771号