SA 后缀数组
后缀数组 SA
定义
我们先定义如下两个数组:
- \(sa_i\) 代表排名为 \(i\) 的后缀的编号;
- \(rk_i\) 代表编号为 \(i\) 的后缀的排名。
此处排名指的是将所有后缀按字典序从小到大排序。
那么根据定义,显然可得结论:\(rk_{sa_i} = sa_{rk_i} = i\)。
举例:
对于字符串 $s = $ abac
,有
后缀 | \(rk\) | |
---|---|---|
\(suf(1)\) | abac |
\(1\) |
\(suf(2)\) | bac |
\(3\) |
\(suf(3)\) | ac |
\(2\) |
\(suf(4)\) | c |
\(4\) |
注:\(suf(i)\) 代表第 \(i\) 个后缀,即从 \(i \sim n\) 的字符串。
那么如上图可得 \(sa = \{1,3,2,4\}\)。
算法
考虑几种做法,排列方式由时间复杂度从劣至优。
\(\mathcal{O(n^2 \log n)}\)
暴力,直接排序。
\(\mathcal{O(n \log^2 n)}\)
这里考虑一种倍增加上排序的方法,我们先算出字符串长度为 \(k = 1\) 时的 \(sa,rk\),然后将 \(k\) 倍增,并合并两个子串的排名,据此计算出最终的 \(sa,rk\)。
具体过程如下:
执行 \(\log_2 n\) 次以下操作。
以字符串子串长度为 \(k / 2\) 时的 \(rk_i\) 为第一关键字,\(rk_{i + k / 2}\) 为第二关键字,进行排序,这样我们可以得到子串长度为 \(k\) 的 \(rk\) 与 \(sa\)。
注意此处一个重点:
\[rk_{i + k / 2} = \left\{\begin{matrix}
-\infty \quad \quad \hfill i + k / 2 > n \\
rk_{i + k / 2} \quad \hfill \text{otherwise.}
\end{matrix}\right.
\]
点击查看代码
il void SA() {
rep(i,1,n) sa[i] = i,rk[i] = s[i];
for (k = 1; k <= n; k <<= 1 ) {
sort(sa + 1,sa + n + 1,[](int x,int y) {
return rk[x] == rk[y] ? rk[x + k] < rk[y + k] : rk[x] < rk[y];
});
swap(rk,rk2);
int tot = 0;
rep(i,1,n) {
if (rk2[sa[i]] != rk2[sa[i - 1]] || rk2[sa[i] + k] != rk2[sa[i - 1] + k]) ++ tot;
rk[sa[i]] = tot;
}
if (tot == n) break;
}
}
\(\mathcal{O(n \log n)}\)
我们考虑在以上倍增算法中的排序上做优化,优化成基数排序可将时间复杂度优化至 \(\mathcal{O(n \log n)}\)。
具体过程如下:
- 我们先以 \(k = 1\) 来算出初始 \(sa,rk\),采用桶 + 前缀和,比如
ababc
,进行前缀和过后 \(cnt_{`a`} = 2,cnt_{`b`} = 4,cnt_{`c`} = 5\),此时 \(cnt_i\) 表示元素 \(i\) 的最大排名,那么我们倒序枚举子串,调用 \(rk_i\) 得到此元素在哪个桶中,再调用 \(cnt_{rk_i}\) 获取单个 \(s_i\) 的排名,并初始化 \(sa\);
rep(i,1,n) ++ cnt[rk[i] = s[i]];
rep(i,1,m) cnt[i] += cnt[i - 1];
rep1(i,n,1) sa[cnt[rk[i]] --] = i;
- 对于 \(s_{n - k + 1,\dots ,n}\),显然他们的第二关键字均为 \(-\infty\),所以我们将其顺序放入 \(rk2\) 即存放第二关键字的数组中,再顺序遍历 \(sa_i\),因为我们是做倍增合并,所以当且仅当 \(sa_i > k\) 时,才能够作为编号为 \(sa_i - k\) 的子串的第二关键字,因为是顺序遍历的 \(sa\),所以此时 \(rk2\) 存放的就是以第二关键字排序好的子串编号;
int tot = 0;
rep(i,n - k + 1,n) rk2[++ tot] = i;
rep(i,1,n) if (sa[i] > k) rk2[++ tot] = sa[i] - k;
- 将当前状态的 \(rk\) 放入桶中,作前缀和,此时我们仅按照第一关键字来排了序,需要再使用第二关键字来排序,在桶中优先枚举第二关键字大的即可。
il void SA() {
m = 200;
rep(i,1,n) ++ cnt[rk[i] = s[i]];
rep(i,1,m) cnt[i] += cnt[i - 1];
rep1(i,n,1) sa[cnt[rk[i]] --] = i;
for (int k = 1; k <= n; k <<= 1 ) {
int tot = 0;
rep(i,n - k + 1,n) rk2[++ tot] = i;
rep(i,1,n) if (sa[i] > k) rk2[++ tot] = sa[i] - k;
rep(i,1,m) cnt[i] = 0;
rep(i,1,n) ++ cnt[rk[i]];
rep(i,1,m) cnt[i] += cnt[i - 1];
rep1(i,n,1) sa[cnt[rk[rk2[i]]] --] = rk2[i];
swap(rk,rk2);
rk[sa[1]] = 1; tot = 1;
rep(i,2,n) {
if (rk2[sa[i]] != rk2[sa[i - 1]] || rk2[sa[i] + k] != rk2[sa[i - 1] + k]) ++ tot;
rk[sa[i]] = tot;
}
if (tot == n) break;
m = tot;
}
}
我们可以对比一下两条提交记录:
时间快了将近 \(3\) 倍。
SA 可以通过拼接串等技巧解决许多问题。
应用和 \(height\) 先鸽一段时间。