字符串 2

7 minute read

Published:

SA

P3809 【模板】后缀排序

把一个字符串的后缀按字典序排序。按排名输出后缀位置。

题目要求的东西记为 $sa$ 数组,其反函数为 $rk$ 数组。

例如:eaabd,其后缀为 eaabd aabd abd bd d,排序后为 aabd abd bd d eaabd,$sa = \langle 2,3,4,5,1 \rangle, rk = \langle 5,1,2,3,4 \rangle$。

二分哈希直接排序当然也是 $O(n \log^2 n)$ 的,但是我们有更靠谱的做法。

以下内容来源:我自己的 SA 学习笔记

求解考虑倍增。

初始轮,$w=1$,把 $sa$ 按每个后缀开头的字符排序。

接下来的每一轮,$w \leftarrow w \times 2$,这次的串 $x$ 应该由上一轮的 $[x,x+w)$ 和 $[x+w,x+2w)$ 两个后缀的前缀拼成。把 $sa$ 以 $rk_x$ 为第一关键字,$rk_{x+w}$ 为第二关键字排序,其中 $rk$ 为由上一轮的排序的 $sa$ 得到的 $rk$。

时间复杂度 $O(n \log^2 n)$,如果用基数排序可以做到 $O(n \log n)$。

int sa[MAXN], rk[MAXN * 2], tmp[MAXN * 2];
// 因为有 +w,开大一倍防止越界
// 因为在统计这一轮的 rk 时还要用到上一轮的 rk,所以这一轮的 rk 先放在 tmp 里,结束后 copy
void build_SA(string S) {
    rep(i, 1, n)
        sa[i] = i, rk[i] = S[i];
    for (int w = 1; w <= n; w <<= 1) {
        auto cmp = [&](int x, int y) -> bool {
            if (rk[x] == rk[y])
                return rk[x+w] < rk[y+w];
            return rk[x] < rk[y];
        };
        sort(sa+1, sa+n+1, cmp);
        rep(i, 1, n)
            tmp[sa[i]] = tmp[sa[i-1]] + cmp(sa[i-1], sa[i]);
        copy(tmp+1, tmp+n+1, rk+1);
    }
}

TODO: SAM 似乎也可以求 SA,会了再回来补。

Height 数组

定义:$h_i = \text{lcp}(sa_i, sa_{i-1})$。特别地,$h_1 = 0$。

Lemma $h[rk_i] \ge h[rk_{i-1}] - 1$。证明略。

基于这个引理,我们按 $rk$ 顺序暴力求出 $h$,均摊后总复杂度就是 $O(n)$。

int k = 0;
rep(i, 1, n) {
    int j = sa[rk[i] - 1];
    if (k) k--;
    while (S[i + k] == S[j + k])
        k++;
    h[rk[i]] = k;
}

P2408 不同子串个数

求字符串 $S$ 的本质不同子串数量。

$\lvert S \rvert = n \le 10^5$。

按照 $sa$ 的顺序依次求解。$sa_{i-1}$ 和 $sa_i$ 的重复前缀数量为 $h_i$。

因此,答案为

\[\frac {n (n+1)} 2 - \sum\limits_{i=2}^n h_i\]

记得开 long long

SAM

P3804 【模板】后缀自动机(SAM)

提供一个 SAM 可视化网址:SAM Drawer

搭建一个自动机,接受且仅接受 $S$ 的所有后缀,且节点数最少。这个自动机称为后缀自动机(SAM),还有一个名字叫 Directed Acyclic Word Graph (DAWG)。

可以证明,节点数是 $O(n)$ 的。

1984 最新最热算法!

首先有一个小结论:把 SAM 上的所有节点都算成接受节点,SAM 就会变成“子串自动机”,接受且仅接受原串的子串。很简单,每个后缀都是从空串所在的节点一步一步转移过来的,所以肯定能经过每个后缀的前缀。后缀的前缀就是子串。

也就是说,SAM 其实是所有子串而不仅仅是所有后缀的集合体。

我们有一个 insight:一个子串会在原串中出现许多次(比如 $\texttt{aba}$ 在 $\texttt{abaababa}$ 的三个位置),这个子串的后缀也必然在这些位置出现,而且可能在更多位置出现(比如 $\texttt{aba}$ 的后缀 $\texttt{a}$ 相对而言就多出现 $2$ 次),这里有一种隐含的“父子关系”的感觉。

根据这个直觉,我们定义一个字符串的 right 集合(也叫 $\text{endpos}$ 集合)为它出现的每个区间的右端点。比如 $\texttt{aba}$ 在 $\texttt{abaababa}$ 的 right 集合是 ${3, 6, 8}$。同理,$\texttt{ba}$ 的 right 集合也是 ${3, 6, 8}$。我们把 right 集合相同的子串称为在同一等价类。不妨把 $a$ 与 $b$ 在同一等价类记作 $a \sim b$。

在一个等价类 $u$ 中,令最长串的长度是 $\text{len}(u)$,最短串的长度是 $\min(u)$。

Lemma 1 不妨设 $\lvert a \rvert \le \lvert b \rvert$。要么 $\text{endpos}(b) \subseteq \text{endpos}(a)$(当且仅当 $a$ 是 $b$ 的后缀),要么 $\text{endpos}(a) \cap \text{endpos}(b) = \varnothing$。

Lemma 2 在一个等价类 $u$ 中,所有串长度刚好覆盖 $[\min(u), \text{len}(u)]$ 区间。

接下来我们刻画前面说的父子关系。在前面的例子中 $\texttt{a}$ 所在的等价类 ${1,3,4,6,8}$,就可以看作是 $\texttt{aba}$ 和 $\texttt{ba}$ 所在的等价类 ${3, 6, 8}$ 的父亲。也就是说 $\text{endpos}$ 的 $\subset$ 关系相当于等价类的父子关系。也因此,这种父子关系构成一棵树,称作后缀链接树,也叫 parent 树。对于一个等价类 $u$,我们记它的父亲为 $fa_u$(有的人记作 $link_u$)。特别地,空串对应的等价类的 $fa$ 为 NULL。

可以发现一个至关重要的性质:等价类的总数是 $O(n)$ 的。可以类比虚树的构造,在已有 $n$ 个叶子($n$ 个后缀)的情况下,这样树最多也只能有 $2n-1$ 个节点。

Lemma 3 $\min(u) = \text{len}(fa_u) + 1$。

* 根据 CF 的那篇文章,$S$ 的后缀链接树是 $S$ 反转后的后缀树(Suffix Tree)。所以也有人把后缀链接树叫做前缀树(Prefix Tree)。

SAM 构造

感性理解一下,以后缀链接树的等价类作为节点产生的 DFA,应当也是最小的 DFA。主要是我不会证

SAM 的每个节点都是一个等价类,因此 SAM 的节点数量是 $O(n)$ 的。

对于每一个节点,维护 $\delta$、$fa$ 和 $\text{len}$,不维护 $\text{endpos}$。$\delta(u,c)$ 存在的充要条件是:$u$ 中的至少一个字符串,在加上 $c$ 之后仍然是 $S$ 的子串

我们采取在线的构造,一开始只有一个空串等价类,每次在字符串末尾添加一个字符并修改 SAM。注意时间复杂度是均摊的。

令 $last$ 表示当前时刻整个字符串 $S$ 所在的等价类。接下来要添加一个字符 $c$,把 $S$ 的 SAM 改造为 $S+c$ 的 SAM。

  1. 首先必然要新建一个 $S+c$ 所在等价类的节点 $cur$。显然 $\text{len}(cur) = \lvert S \rvert + 1 = \text{len}(last) + 1$。而且 $cur$ 必然在原先的自动机内是不存在的。
  2. 沿着后缀链接树往上爬。
    • 令 $p=last$,若 $\delta(p,c)$ 是 NULL,则 $\delta(p,c) \gets cur$,然后 $p \gets fa_p$ 这样往上爬。
    • 重复这个过程直到找到一个有效的 $\delta(p,c)$ 或者 $p$ 是 NULL。
  3. 若 $p$ 是 NULL 即已经爬到根了还没找到,这种情况只可能出现在加入了一个没出现过的字符,令 $fa_{cur}$ 为根节点即可。
  4. 若找到了一个 $q = \delta(p,c)$,相对复杂一点。在这里细分一下情况:

Case 1: $\text{len}(q) = \text{len}(p) + 1$

$fa_{cur} \gets q$ 即可。

既然 $\delta(p,c)$ 存在,意味着 $S$ 末尾长为 $\text{len}(p)$ 的后缀的一部分出现位置后面一个字符正好是 $c$ 能接上,这些串(加上 $c$ 之后)的 $\text{endpos}$ 即为 $q$ 这个节点。

$\text{endpos}(cur) \subseteq \text{endpos}(q)$,而且 $q$ 作为第一个找到的点满足 $\text{len}(q)$ 最长,因此 $fa_{cur} \gets q$。

(在 $\texttt{aab}$ 后面插入一个 $\texttt{a}$。黑色是后缀链接树,蓝色是 SAM;橙色是 $p$,紫色是 $q$)

Case 2: $\text{len}(q) > \text{len}(p) + 1$

* 为什么是大于而不是不等于:这个地方不可能出现 $\text{len}(q) < \text{len}(p) + 1$,因为 $\text{len}(p)$ 对应的串 $+c$ 之后一定出现在 $q$ 里。

此时 $q$ 其实混了一堆东西:有从 $\text{len}(p)$ 对应的串 $+c$ 之后得到的 $\text{len}(p) + 1$,也有比 $\text{len}(p)$ 更长的串 $+c$。

(在 $\texttt{aaba}$ 后面插入一个 $\texttt{b}$。)

TODO: 还没懂!

新建一个 $clone$ 节点,继承 $q$ 的所有 $\delta$,且 $fa_{clone} \gets fa_q$,唯一不同的是 $\text{len}(clone) = \text{len}(p) + 1$。

从 $p$ 沿着后缀链接树往上爬,令爬到的节点为 $u$,把所有 $\delta(v,c) = q$ 全都改成抵达 $\delta(u,c) = clone$。注意只要有 $\delta(v,c)$ 不存在,$u$ 的所有祖先也不会有 $c$ 的出边,要及时退出。

最后 $fa_{cur} \gets clone$,$fa_q \gets clone$。

实现

实现时间复杂度空间复杂度
数组$O(n)$$O(n \lvert \Sigma \rvert)$
平衡树$O(n \log \lvert \Sigma \rvert)$$O(n)$
哈希表$O(n)$$O(n)$

但是 unordered_map 真的常数巨大而且可能被卡,所以这里使用了折中的平衡树 map。后面例题的时间复杂度分析也使用 map 版本。

实践中,小字符集用数组,大字符集用 map,哈希表不用考虑(乐)。

SAM 的节点数量是不超过 $2n-1$ 的,所以要开 $2$ 倍数组。

struct SAM {
    struct Node {
        int len, fa;
        map<char, int> nxt;
    } t[MAXN * 2];
    int tot, last;
    SAM() {
        t[0].len = 0, t[0].fa = -1;
        tot = 0;
        last = 0;
    }
    int newNode() {
        return ++tot;
    }
    void extend(char c) {
        int cur = newNode();
        t[cur].len = t[last].len + 1;
        int p = last;
        while (p != -1 && !t[p].nxt.count(c)) {
            t[p].nxt[c] = cur;
            p = t[p].fa;
        }
        if (p == -1)
            t[cur].fa = 0;
        else {
            int q = t[p].nxt[c];
            if (t[q].len == t[p].len + 1)
                t[cur].fa = q;
            else {
                int clone = newNode();
                t[clone].nxt = t[q].nxt;
                t[clone].fa = t[q].fa;
                t[clone].len = t[p].len + 1;
                while (p != -1 && t[p].nxt[c] == q) {
                    t[p].nxt[c] = clone;
                    p = t[p].fa;
                }
                t[q].fa = t[cur].fa = clone;
            }
        }
        last = cur;
    }
};

时间复杂度证明

结论:$n \ge 2$ 时,SAM 状态数不超过 $2n - 1$;$n \ge 3$ 时,转移数不超过 $3n - 4$。这两个上界都是紧的,构造分别是 $\texttt{abb} \cdots \texttt{bb}$ 和 $\texttt{abb} \cdots \texttt{bbc}$。

TODO:

Ref

名人堂属于是。

SAM 维护 endpos

SAM 的节点只维护 $\delta$、$fa$ 和 $\text{len}$,并不直接维护 $\text{endpos}$,但是在一些题目当中我们又确实需要查询 $\text{endpos}$ 相关的信息。

见下面例题中的 P4094 [HEOI2016/TJOI2016] 字符串。

定位子串所在的节点

相当于找 $S_{[1,r]}$ 的祖先中第一个 $\text{len} \ge r-l+1$ 的。倍增即可,时间复杂度 $O(\log n)$。

例题

P3804 【模板】后缀自动机(SAM) (Easy)

求出 $S$ 的所有出现次数不为 $1$ 的子串的出现次数乘上该子串长度的最大值。

$\lvert S \rvert = n \le 10^6$,$\lvert \Sigma \rvert = 26$。

注意到我们要求:

\[\max\limits_{u} \left( [\lvert \text{endpos}(u) \rvert \ne 1] \times \lvert \text{endpos}(u) \rvert \times \text{len}(u) \right)\]

问题转化为求 $\lvert \text{endpos}(u) \rvert$。

相当于求 $u$ 在后缀链接树上有几个后代含有 $S_{[1,x]}$,只要每次插入新字符时,创建的新节点 $siz$ 设为 $1$,clone 出来的节点 $siz$ 设为 $0$,DFS 一遍求子树大小即可。

P4070 [SDOI2016] 生成魔咒【强制在线】 (Easy)

有一个空串。$n$ 次操作,每次在串的末尾加一个字符。每次操作结束后求出当前本质不同子串数量。

$n \le 10^5$,$\lvert \Sigma \rvert = 10^9$。

若没有强制在线,这题有 SA 的做法,但是 SAM 的做法是更加自然且显然的。

在 SAM 上,对于一个新的状态 $u$,新加的本质不同子串个数是 $\text{len}(u) - \text{len}(fa_u)$。做完了。

P8368 [LNOI2022] 串

给定一个英文小写字母构成的字符串 $S$,你需要找到一个尽可能长的字符串序列 $(T_0, T_1, \ldots, T_l)$,满足:

  • $T_0$ 是 $S$ 的子串;
  • $\forall 1 \le i \le l$,$\lvert T_i \rvert - \lvert T_{i - 1} \rvert = 1$;
  • $\forall 1 \le i \le l$,存在 $S$ 的一个长度为 $\lvert T_i \rvert + 1$ 的子串 $S’i$,使得 $S’_i$ 的长度为 $\lvert T{i - 1} \rvert$ 的前缀为 $T_{i - 1}$,长度为 $\lvert T_i \rvert$ 的后缀为 $T_i$。

输出这样的字符串序列的长度的最大值(即 $l$ 的最大值)。

多组数据,$1 \le \lvert S \rvert \le 5 \times {10}^5$,$1 \le \sum \lvert S \rvert \le 1.5 \times {10}^6$。

TODO:

P4094 [HEOI2016/TJOI2016] 字符串 (Hard)

超级标准的 SA / SAM 套路题。

给出字符串 $S$,多次询问 $a,b,c,d$ 求 $S_{[a,b]}$ 的所有子串与 $S_{[c,d]}$ 的最长公共前缀的最大值。

$n,m \le 10^5$。$\lvert \Sigma \rvert = 26$。

不管是哪条路,我们都要先发现一个性质,就是答案是有可二分性的。二分一下转成判定性问题。

设 $len$ 为当前二分到的需要判定的长度。

支路 1:SA

LCP 和 SA 是非常搭的。接下来转化问题就要往后缀上靠。

我们现在要找一个 $S$ 的子串 $s$:

  • 首先 $s$ 的开头要在 $[a, b - len + 1]$ 内。
  • 其次 $\text{lcp}(s, S_{[c,d]}) \ge len$,或者等价说法 $\text{lcp}(s, S_{[c,n]}) \ge len$,转化成后缀问题(顺便清掉了 $d$ 这个变量)。
    • 不难发现,在 $sa$ 的顺序上,这是一个包含 $rk_c$ 的连续区间。再二分一次求出这个区间 $[l,r]$,用 height 数组判定。

接下来我们要查询 $sa_{[l,r]}$ 内是否有 $[a, b - len + 1]$ 内的任何一个数。

对于这个问题我们可能会想到离线扫描线,但是二分是不能离线的!所以只能在线可持久化线段树。

二分套可持久化线段树,时间复杂度 $O(M(n) + m \log^2 n)$($M(n)$ 为搭建 SA 的时间复杂度),与 $\lvert \Sigma \rvert$ 无关。

Ref:

支路 2:SAM

SAM 这么强大,肯定还是得试试的!

我们要判断 $s_{[c, c + len - 1]}$ 是否在 $s_{[a,b]}$ 出现过。都建出 SAM 了,放到后缀链接树上考虑一下。

首先要找到 $s_{[c, c + len - 1]}$ 所在的状态 $u$,倍增即可。

接下来我们想要知道是否 $\text{endpos}(u) \cap [a + len - 1, b] = \varnothing$。但是上面讲 SAM 的时候我们好像没有维护 $\text{endpos}$ 啊?

SAM 是可以维护 $\text{endpos}$ 的!$\text{endpos}$ 相当于一个值域数组,我们要给树上每个节点都开一个值域数组,这样的需求用线段树合并可以实现。

TODO: 一定写一下!

P4770 [NOI2018] 你的名字 (Lunatic)

牛。

这题的不超纲做法应该是 SA,但是强大的 SAM 依然可以做这题!

给定一个串 $S$。 $q$ 次询问,每次给定一个串 $T$ 和一个区间 $[l,r]$,求 $T$ 有几个本质不同子串没有在 $S_{[l,r]}$ 出现。

$\lvert S \rvert = n \le 5 \times 10^5$,$q \le 10^5$,$\sum \lvert T \rvert \le 10^6$,$\lvert \Sigma \rvert = 26$。

正难则反,我们算 $T$ 有几个本质不同子串在 $S_{[l,r]}$ 出现了

Subtask: $l=1, r=n$

SAM 是可以维护 $\text{endpos}$ 的

TODO:

P7361 「JZOI-1」拜神 (Lunatic)

给定一个串 $S$。$q$ 次询问,每次给定一个区间 $[l,r]$,求 $S_{[l,r]}$ 出现至少两次的子串的长度最大值。

$\lvert S \rvert = n \le 5 \times 10^4$,$q \le 10^5$,$\lvert \Sigma \rvert = 26$。

TODO:

广义 SAM

SAM 与广义 SAM 的关系,和 (K)MP 与 ACAM 的关系是一样的。

常见错误

TODO:

广义 SAM 的构造

P6139 【模板】广义后缀自动机(广义 SAM)

离线

和 AC 自动机一模一样。

TODO:

在线

TODO:

广义 SAM 例题

P4081 [USACO17DEC] Standing Out from the Herd P (Easy)

TODO:

TODO: https://www.luogu.com.cn/article/pmhi8zkx https://www.luogu.com.cn/article/4wdm8d18

Tags: