2023-03-17- 后缀自动机

发布时间 2023-07-05 12:03:36作者: Iridescent41

我直接忽略掉这个玩意的原理。

或许我应该从自动机开始,更确切地说我应该从 确定有限状态自动机 (\(DFA\)) 开始。

\(\mathbb{DFA}\)

\(\mathtt{Definition}\)

首先给出一些前置的定义。

  • \(Q\),有限状态集合。
  • \(\Sigma\),有限字符集。
  • \(\delta\),转移函数, \(Q \times E \rightarrow Q\)
  • \(q_0\),起始状态。
  • \(F \subset Q\),接受状态集合。

\(N = (Q, \Sigma, \delta, q_0, F)\) ,定义 \(E(q)\) 表示从 状态 \(q\) 出发,只沿着 \(\epsilon\) 转移能够到达的集合。
然后构造 \(M = (Q', \Sigma, \delta', E(q_0), F')\),其中

  • \(Q'\),状态集合, \(Q' = \mathcal{P}(Q)\)
  • \(\delta'\),转移函数,\(Q' \times E \rightarrow Q',\delta'(S, c) = \bigcup q \in S, q' \in \delta(q, c) E(q')\)
  • \(F'\),终止状态集合,\(F' = \{ S \subset Q \mid S \cap F \not = \empty \}\)

工作时自动机会一个一个读入字符集,按照 \(\delta\) 转移,如果在转移结束之后处于一个接受状态,那么成这个自动机接受这个字符串,反之称这个自动机不接受这个状态。

当一个状态 \(S\) 没有字符 \(c\) 的转移时,令 \(\delta(S, c) = \mathrm{null}\)\(\mathrm{null}\) 不属于任何接受状态,也无法进行转移。

于是我们可以简单地定义自动机了。

\(\mathbb{SAM}\)

\(\mathtt{Definition}\)

一个字符串的 后缀自动机(\(\mathrm{SAM}\) 就是一个 接受且仅接受 这个字符串的 所有后缀\(DFA\)

那么根据定义,如果一个字符串 \(t\)\(s\) 的后缀,当且仅当 \(\mathrm{SAM}_{s}(t) = \mathtt{true}\) ,同样可以知道,如果一个字符串 \(t\)\(s\) 的子串,当且仅当 \(\delta_s(t) \not= \mathrm{null}\) ,很好理解,因为如果在 \(t\) 的后面增加一些字符成为 \(t'\)\(t'\) 一定是 \(s\) 的后缀。

\(\mathtt{Construction}\)

考虑一下,还是先写构造的方式。

\(\Theta(n^2) \ \mathrm{SAM}\)

首先看一个很简单的 \(\mathrm{SAM}\) ,即是说建一棵字典树,直接把所有后缀塞进去,每次标记终止节点为接受状态,就可以得到一个状态数 \(\Theta(n^2)\)\(\mathrm{SAM}\)

\(\Theta(n) \ \mathrm{SAM}\)

然后应用关于自动机的一些科技,对这个玩意进行 任意 \(DFA\) 压缩
首先写对 \(\Theta(n^2) \ \mathrm{SAM}\) 的压缩,最后写个泛化的压缩方式。

下面定义一些变量。

\(\mathtt{right \ \& \ reg}\)

注意到这个是个集合,对于一个字符串 \(t\) ,如果它在 \(s\) 中出现的位置的集合为 \(\{ [l_1, r_1), [l_2, r_2), \dots [l_n, r_n)\}\) ,那么 \(\mathtt{right}(t) = \{ r_1, r_2, \dots, r_n \}\)

定义 \(\mathtt{right}\) 集合的等价类为所有 \(\mathtt{right}\) 集合相等的字符串, \(\mathtt{reg}(v)\) 表示从状态 \(v\) 开始能识别的字符串的集合,就是说 \(t \in \mathtt{reg}(v) \Leftrightarrow \delta (v, t) \in F\)

这时我们给 字符串 \(t\) 后面接上一个字符串 \(s[r_i, n], r_i \in \mathtt{right}(t)\) 变成 \(t'\) 使得 \(t'\) 成为 \(s\) 的后缀,所以如果 \(\mathtt{right}(t_1) = \mathtt{right}(t_2)\) ,那么 \(\mathtt{reg}\mathrm{(SAM)}(t_1) = \mathtt{reg}\mathrm{(SAM)}(t_2) = s[r_i, n], r_i \in \mathtt{right(t_1)}\)

这样的 \(\mathrm{SAM}\) 的状态数是最少的当且仅当 \(\mathtt{reg}\) 集合对应的状态两两不同。

\(\mathtt{minl \ \& \ maxl}\)

表示 这个状态下对应的最长的字符串和最短的字符串
根据定义可以知道 \(v\) 对应的任意一个字符串都是 \(\mathtt{maxl}(v)\) 的后缀,且不是 \(\mathtt{minl}(v)\) 的真后缀。并且,\(v\) 包含了所有这样的字符串。

\(\mathtt{parent}\)

后面简写为 \(\mathtt{par}\)
对于每个状态 \(v\)\(\mathtt{minl}(v)[1, n - 1]\) 对应的状态。
根据定义可知 \(\mathtt{len}(\mathtt{minl}(v)) = \mathtt{len}(\mathtt{maxl}(v)) + 1\) ,并且可以发现由指向 \(\mathtt{par}\) 的边可以构成一棵树,称为 \(\mathtt{par}\) 树。
此时 \(\mathrm{SAM}\) 上的接受状态就是包含 \(r_i = n\) 的一些 \(\mathtt{right}\) 集合的等价类,那么反过来我们可以通过 \(\mathtt{par}\) 树确定 \(\mathrm{SAM}\) 上面可接收状态的集合。

\(\mathtt{Code}\)

struct SuffixAutomaton {
  int siz, last;
  LL tot;
  struct Node { int len, par; map<char, int> nxt; } s[MAXN << 1];
  void init() { s[0].len = 0, s[0].par = -1, siz = 1, last = 0; }
  void extend(char c) {
    int cur = ++siz;
    s[cur].len = s[last].len + 1;
    int p = last;
    while (p != -1 && !s[p].nxt.count(c)) {
      s[p].nxt[c] = cur;
      p = s[p].par;
    }
    if (p == -1) {
      s[cur].par = 1;
    } else {
      int q = s[p].nxt[c];
      if (s[p].len + 1 == s[q].len) {
        s[cur].par = q;
      } else {
        int clone = ++siz;
        s[clone].len = s[p].len + 1;
        s[clone].nxt = s[q].nxt;
        s[clone].par = s[q].par;
        while (p != -1 && s[p].nxt[c] == q) {
          s[p].nxt[c] = clone;
          p = s[p].par;
        }
        s[q].par = s[cur].par = clone;
      }
    }
    last = cur;
    tot += s[cur].len - s[s[cur].link].len;
  }
} sam;

\(\mathtt{DFA \ minimization}\)