后缀数组相关
后缀数组
基础的数组有两个:\(\text{sa}[]\) 和 \(\text{rk}[]\)。\(\text{sa}[i]\) 表示“第 \(i\) 小的后缀是第几个后缀”,\(\text{rk}[i]\) 表示“第 \(i\) 个后缀是第几小的”。这两个数组可以通过排序产生。
考虑双关键字倍增排序。初始化把所有长度为 \(1\) 的子串排个序,得到一个 \(\text{sa}[]\) 和 \(\text{rk}_ 1[]\)。然后用 \(\text{rk}_ 1[i]\) 和 \(\text{rk}_ 1[i+1]\) 作为当前 \(i\) 的关键字对长度为 \(2\) 的子串排序,得到一个 \(\text{sa}[]\) 和 \(\text{rk}_ 2[]\)。然后用 \(\text{rk}_ 2[i]\) 和 \(\text{rk}_ 2[i+2]\) 作为当前 \(i\) 的关键字对长度为 \(4\) 的子串排序,得到一个 \(\text{sa}[]\) 和 \(\text{rk}_ 4[]\)……。倍增到最后得到一个后缀数组 \(\text{sa}[]\) 和 \(\text{rk}_ n[]\)。
使用基数排序,每轮排序的复杂度是 \(O(n)\) 的。由于进行了 \(O(\log n)\) 次,因此复杂度是 \(O(n \log n)\) 的。
Submission loj的设计就很优美
code
const int N = 1e6 + 10;
char ch[N];
int n, sa[N], rk[N];
void getsa(char ch[], int sa[], int rk[]) {
int m = 127;
static int cnt[N], rk2[N], key[N], id[N];
memset(cnt+1, 0, m << 2);
rep(i,1,n) rk[i] = ch[i], cnt[rk[i]]++;
rep(i,1,m) cnt[i] += cnt[i-1];
pre(i,n,1) sa[cnt[rk[i]] --] = i;
for (int w = 1; ; w <<= 1) {
int p = 0;
for (register int i = n; i > n - w; -- i) id[++p] = i;
rep(i,1,n) if (sa[i] > w) id[++p] = sa[i] - w;
memset(cnt + 1, 0, m << 2);
rep(i,1,n) key[i] = rk[id[i]], cnt[key[i]]++;
rep(i,1,m) cnt[i] += cnt[i-1];
pre(i,n,1) sa[cnt[key[i]] --] = id[i];
memcpy(rk2+1, rk+1, n << 2);
p = 0;
rep(i,1,n) rk[sa[i]] = (
rk2[sa[i]] == rk2[sa[i-1]] and
rk2[sa[i] + w] == rk2[sa[i-1] + w]
) ? p : ++p;
if (p == n) { rep(i,1,n) sa[rk[i]] = i; break; }
m = p;
}
}
当然你可以 \(O(n)\) 排序。有两种方法:DC3 和 SA-IS。DC3 难写常数还大,所以基本上没人用。这里介绍 SA-IS。
IS 是诱导排序,因为 cache 很友好所以常数小。而且码量不大
下面称从 \(i\) 位置开始的后缀为 后缀 \(i\)
我们在字符串最后加上去一个字典序小于字符串内任意字符的新字符 \(\#\)。将后缀分为两类:S 类和 L 类。有:
- 新加入的字符为 S 类。
- 若从 \(i\) 位置开始的后缀字典序严格小于从 \(i+1\) 开始的后缀,则从 \(i\) 位置开始的后缀为 S 类,反之为 L 类。
后缀的类型可以倒着扫一遍得到。具体地,若当前字符与后一位字符不同,则该后缀的类型可得;若相同则该后缀的类型与后一个后缀相同。
若后缀 \(i\) 为 S 类,后缀 \(j\) 为 L 类且字符 \(i =\) 字符 \(j\),则后缀 \(i\) 的字典序大于后缀 \(j\)。
然后排序。我们定义 lms(Left-Most-S-suffix) 串为左边是一个 L 型串的 S 型串。显然有 lms 串的数量最多是 \(\frac n2\) 的。定义 lms 子串为两个 lms 串中间(包含两端)的串。SA-IS 通过把原串拆成 lms 子串的方式缩小求解范围。
我们递归求得下层的 \(\text{sa}[]\),现在需要求得本层的 \(\text{sa}[]\)。使用诱导排序。步骤:
- 使用两部分桶记录 S 串和 L 串的数量
- 倒序扫 lms,放进它的 sa 对应的 S 型桶里
- 正序扫 sa,如果 sa - 1 位置的类型是 L 型就放进对应的 L 型桶里
- 倒序扫 sa,如果 sa - 1 位置的类型是 S 型就放进对应的 S 型桶里
有结论说明 lms 子串进行一遍诱导排序后就有序了。
然后考虑如何对 lms 离散化。如果首位不同则编号,相同则暴力匹配。
- 确定每个后缀的类型,找出lms类型后缀。
- 第一遍诱导排序,对lms排序。
- 对lms离散化。
- 如果lms互不相同,直接计算sa,否则递归。
- 恢复lms的位置,诱导排序一遍计算sa。
好 SA-IS 我之后再填吧
height
\(height[i] = \text{lcp}(\text{sa}[i],\text{sa}[i-1])\)。
引理:\(height[\text{rk}[i]] \ge height[\text{rk}[i-1]]-1\)。
然后我们就可以顺着 \(\text{rk}\) 数组扫一遍得到答案。
void getheight() {
for (int i = 1, k = 0; i <= n; i++) {
if (rk[i] == 0) continue;
if (k) k--;
while (s[i + k] == s[sa[rk[i] - 1] + k]) k++;
height[rk[i]] = k;
}
}
定理:\(\text{lcp}(sa[i],sa[j]) = \min_{k=i}^j height[k]\)
所以可以在 height 数组上建一个st表得到lcp。
前面的区域,以后再