后缀数组(SA)学习笔记

SuperSupper·2024-08-05 16:43·28 次阅读

后缀数组(SA)学习笔记

为什么不叫前缀数组呢

基本数组#

\(sa_i (suffix~array)\) 指字符串 \(s\) 中,排名为 \(i\) 的后缀从哪一位开始

\(rk_i (rank)\) 指字符串 \(s\) 从第 \(i\) 位开始的后缀在所有后缀中的排名

\(h_i (height)\) 指字符串 \(s\) 中,从第 \(i\) 位开始的后缀与排名为 \(rk_{i} - 1\) 的后缀的最长公共前缀

求解方法#

对于 \(sa\)\(rk\) 数组经常一起求解,解这两个数组通常使用倍增方法求。

首先预处理出以每个后缀第一个字符进行排序的 \(sa\)\(rk\) 数组

接下来进行倍增

  1. \(sa_i\)\(sa_{i + d}\) 接起来
  2. 排序
  3. 计算 \(rk\) 数组

模拟运行过程:

具体实现时排序可以使用计数排序,为了稳定性,将第二关键字倒序加入即可

时间复杂度:\(O(n \log n)\)

模板:洛谷 P3809

Code
Copy
#include <algorithm> #include <iostream> #include <numeric> using namespace std; const int kN = 1e6 + 1; int n, sa[kN], rk[2 * kN]; string s; void I() { int b[kN], c[kN]; fill(b, b + kN, 0); iota(sa + 1, sa + n + 1, 1); sort(sa + 1, sa + n + 1, [](int x, int y) { return s[x] < s[y]; }); for (int i = 1; i <= n; i++) { rk[sa[i]] = rk[sa[i - 1]] + (s[sa[i]] != s[sa[i - 1]]); } for (int d = 1; d < n; d <<= 1) { for (int i = 1; i <= d; i++) { b[i] = n - d + i; } for (int i = 1, m = d; i <= n; i++) { (sa[i] > d) && (b[++m] = sa[i] - d); } fill(c, c + kN, 0); for (int i = 1; i <= n; i++) { c[rk[i]]++; } for (int i = 1; i <= n; i++) { c[i] += c[i - 1]; } for (int i = n; i >= 1; i--) { sa[c[rk[b[i]]]--] = b[i]; } for (int i = 1; i <= n; i++) { int t = rk[sa[i]] != rk[sa[i - 1]] || rk[sa[i] + d] != rk[sa[i - 1] + d]; b[sa[i]] = b[sa[i - 1]] + t; } copy(b + 1, b + n + 1, rk + 1); } } int main() { cin.tie(0)->sync_with_stdio(0); cin >> s, n = s.size(), s = 's' + s; I(); for (int i = 1; i <= n; i++) { cout << sa[i] << ' '; } return 0; }

\(h\) 数组的求解#

如果你注意力惊人的话,可以注意到 \(h_{rk_i} \geq h_{rk_{i - 1}} - 1\)

注意力不够的话看这里 我也不会证:)

oi-wiki 上的证明

然后可以写出 \(O(n)\) 的代码计算

Copy
for (int i = 1, x = 0; i <= n; i++) { int j = sa[rk[i] - 1]; for (x = max(0, x - 1); max(i, j) + x <= n && s[i + x] == s[j + x]; x++) { } h[i] = x; }

模板:P10469 后缀数组

Code
Copy
#include <algorithm> #include <iostream> #include <numeric> #include <set> using namespace std; using ll = long long; using pll = pair<ll, ll>; const int kN = 1e6 + 1; int n, a[kN], f[kN], h[kN], sz[kN], mx[kN], sa[kN], rk[kN]; set<int> st; pll ans[kN]; string s; void I() { int b[kN], c[kN]; fill(b, b + kN, 0); iota(sa + 1, sa + n + 1, 1); sort(sa + 1, sa + n + 1, [](int x, int y) { return s[x] < s[y]; }); for (int i = 1; i <= n; i++) { rk[sa[i]] = rk[sa[i - 1]] + (s[sa[i]] != s[sa[i - 1]]); } for (int d = 1; d < n; d <<= 1) { for (int i = 1; i <= d; i++) { b[i] = n - d + i; } for (int i = 1, m = d; i <= n; i++) { (sa[i] > d) && (b[++m] = sa[i] - d); } fill(c, c + kN, 0); for (int i = 1; i <= n; i++) { c[rk[i]]++; } for (int i = 1; i <= n; i++) { c[i] += c[i - 1]; } for (int i = n; i >= 1; i--) { sa[c[rk[b[i]]]--] = b[i]; } for (int i = 1; i <= n; i++) { int t = rk[sa[i]] != rk[sa[i - 1]] || rk[sa[i] + d] != rk[sa[i - 1] + d]; b[sa[i]] = b[sa[i - 1]] + t; } copy(b + 1, b + n + 1, rk + 1); } } int F(int x) { return f[x] == x ? f[x] : F(f[x]); } int main() { cin.tie(0)->sync_with_stdio(0); cin >> s, n = s.size(), s = 's' + s; for (int i = 1; i <= n; i++) { cin >> a[i]; } I(); for (int i = 1, x = 0; i <= n; i++) { int j = sa[rk[i] - 1]; for (x = max(0, x - 1); max(i, j) + x <= n && s[i + x] == s[j + x]; x++) { } h[rk[i]] = x; } for (int i = 1; i <= n; i++) { cout << sa[i] - 1 << ' '; } cout << '\n'; for (int i = 1; i <= n; i++) { cout << h[i] << ' '; } return 0; }

练习:#

P2178 [NOI2015] 品酒大会

给定一个字符串 s,若 \(lcp(suffix_i, suffix_j) \geq r\),则称 \(i\)\(j\)\(r\) 相似。对于每一个 \(0 \le r \le n - 1\),求满足条件的 \((i, j)\) 的数量以及 \(a_i * a_j\) 的最大值

\(n \le 3 * 10^4, |a_i| \le 10^9\)

Solution 对于每一个满足条件的数对,我们将 $i$ 向 $j$ 连边,用并查集处理即可,注意要维护每个块的大小、最大值以及最小值(因为有负数)

但是还会超时,发现倒着枚举 \(r\) 只有连边操作,于是倒着枚举,并根据 \(h\) 从大到小排序

Code 作者太懒了:(

P8023 [ONTAK2015] Tasowanie

给定两个数字串 \(A\)\(B\),通过将 \(A\)\(B\) 进行二路归并得到一个新的数字串 \(T\),请找到字典序最小的 \(T\)

\(n \le 3 * 10^5, m \le 3 * 10^5\)

Solution 直接把第二个串接在第一个串后面跑一边后缀数组贪心归并即可,注意两个串的串尾都要放入无穷大的分隔符
点击查看代码
Copy
#include <algorithm> #include <iostream> #include <numeric> using namespace std; const int kN = 1e6 + 1; int n, m, _n, _m, s[kN], sa[kN], rk[2 * kN]; void I() { int b[kN], c[kN]; fill(b, b + kN, 0); cin >> n, _n = n; for (int i = 1; i <= n; i++) { cin >> s[i]; } s[++n] = 1e9; cin >> m, _m = m; for (int i = n + 1; i <= n + m; i++) { cin >> s[i]; } n += m; s[++n] = 1e9; iota(sa + 1, sa + n + 1, 1); sort(sa + 1, sa + n + 1, [](int x, int y) { return s[x] < s[y]; }); for (int i = 1; i <= n; i++) { rk[sa[i]] = rk[sa[i - 1]] + (s[sa[i]] != s[sa[i - 1]]); } for (int d = 1; d < n; d <<= 1) { for (int i = 1; i <= d; i++) { b[i] = n - d + i; } for (int i = 1, m = d; i <= n; i++) { (sa[i] > d) && (b[++m] = sa[i] - d); } fill(c, c + kN, 0); for (int i = 1; i <= n; i++) { c[rk[i]]++; } for (int i = 1; i <= n; i++) { c[i] += c[i - 1]; } for (int i = n; i >= 1; i--) { sa[c[rk[b[i]]]--] = b[i]; } for (int i = 1; i <= n; i++) { int t = rk[sa[i]] != rk[sa[i - 1]] || rk[sa[i] + d] != rk[sa[i - 1] + d]; b[sa[i]] = b[sa[i - 1]] + t; } copy(b + 1, b + n + 1, rk + 1); } } int main() { cin.tie(0)->sync_with_stdio(0); I(); for (int i = 1, j = _n + 2; i <= _n || j <= _n + _m + 1;) { if (i <= _n && (rk[i] < rk[j] || j > _n + _m + 1)) { cout << s[i++] << ' '; } else { cout << s[j++] << ' '; } } return 0; }

P1117 [NOI2016] 优秀的拆分

给定一个字符串 \(S\),求 \(AABB\) 式的字串数量,其中 \(A\)\(B\)\(S\) 非空子串

\(|S| \le 3 * 10^4\)

Solution 设 $fs_i$ 为以 $i$ 结尾的 AA 串数量,设 $fp_i$ 为以 $i$ 开始的 $AA$ 串数量,则答案为 $\sum\limits_{i = 1}^{n - 1} fs_i \cdot fp_{i + 1}$

考虑如何求解 \(fs\)\(fp\)。枚举 \(len\)\(AA\) 串长度,并每隔 \(len\) 就设置一个检查点,发现对于每一个 \(2 * len\) 的 AA 串都必然穿过相邻的两个 \(AA\)

\(i + l - lcs(i, i + len) < i + lcp(i, i + len)\),如图:

发现对于中间没有交集的部分可能并不相等,所以这种情况不会产生贡献

\(i + l - lcs(i, i + len) \geq i + lcp(i, i + len)\),如图

对于中间的三条分割线,设它们分别为 \(j - len + 1\)\(j\)\(j + len\),根据图片容易发现 \(S_{j - len + 1... j} = S_{j...j + len}\),所以紫色的这一个字串是一个合法的 \(AA\) 串。同理,对于红线与绿线的交集部分,每一个点都可以成为 \(AA\) 串的中心,将两端差分即可得出答案

Code
Copy
还在维修:)
posted @   sb-yyds  阅读(28)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示
目录