SA 和 SAM

开个坑,待补

SAM

SAM 是一个接受 \(s\) 的所有后缀的 \(DFA\),从初始状态 \(t_0\) 出发走到一个终止状态,则路径上所有转移连接起来一定是 \(s\) 的一个后缀。一个很显然的性质是 SAM 包含了 \(s\) 的所有子串,因为子串可以视为后缀的前缀。

然后我们引入一些概念。

\(\operatorname{endpos}\) 和等价类

我们定义 \(\operatorname{endpos}(t)\) 为字符串 \(s\)\(t\) 的所有结束位置集合,对于两个 \(\operatorname{endpos}\) 集合相等的子串 \(t_1, t_2\) 我们称它们属于一个等价类 \(E\)

换句话说,SAM 中的每个状态对应一个或多个 \(\operatorname{endpos}\) 相同的子串。

接下来是一堆引理,都比较显然,感性理解一下。

引理 \(1\)

考虑两个非空子串 \(s_1, s_2\),令 \(|s_1|\geq|s_2|\)

\(1.1\) :若 \(s_1,s_2\)\(\operatorname{endpos}\) 相同,那么 \(s_2\)\(s_1\) 的一个后缀,且 \(s_2\)\(S\) 中每一次都以 \(s_1\) 的后缀的形式出现。

\(1.2\):若 \(s_2\)\(s_1\) 的一个后缀,且 \(s_2\)\(S\) 中每一次都以 \(s_1\) 的后缀的形式出现,那么 \(s_1,s_2\)\(\operatorname{endpos}\) 相同。

引理 \(2\)

考虑两个非空子串 \(s_1, s_2\),令 \(|s_1| \geq |s_2|\)

\(2.1\):若 \(s_2\)\(s_1\) 的一个后缀,则有 \(\operatorname{endpos}(s_1) \subseteq \operatorname{endpos}(s_2)\)

\(2.2\):若 \(s_2\) 不是 \(s_1\) 的一个后缀,则有 \(\operatorname{endpos}(s_1) \cap \operatorname{s_2} = \varnothing\)

引理 \(3\)

一个 \(\operatorname{endpos}\) 等价类不会包括两个本质不同而长度相同的字符串。

引理 \(4\)

\(4.1\) 一个 \(\operatorname{endpos}\) 等价类 \(E\) 中的任意两个子串,要么相同,要么较短者为较长者的真后缀

\(4.2\)\(l, r\) 分别为 \(E\) 内包含最短和最长子串的长度,则 \(\cup |E_i| = {l \leq x \leq r, x \in N^{*}}\)

后缀链接

考虑 SAM 中某个不是初始状态的状态 \(v\),定义 \(w\)\(v\) 对应等价类中最长的一个字符串,则 \(v\) 的后缀链接要连接到字符串 \(t\) 的等价类 \(u\) 上并且满足 \(t\) 为最长的,与 \(w\) 不在同一个等价类的 \(w\) 的一个真后缀。

它的实际意义在于根据引理 \(4.2\),如果从某个等价类 \(E\) 不断跳 \(\operatorname{link}\) 直到初始结点的途中一定可以访问到 \(E\) 中最长子串 \(w\) 的每一个后缀。

引理 \(5\)

设一个 \(\operatorname{endpos}\) 等价类为 \(E\)\(E\) 中最短的串为 \(s\).

\(5.1\)\(link(E)\) 中最长的串是 \(s\) 长度为 \(|s| - 1\) 的后缀。

\(5.2\)\(E \subsetneq link(E)\)

其中 \(5.1\) 转化为:

\(\operatorname{short}(E) = \operatorname{long}(\operatorname{link}(E)) + 1\),其中 \(\operatorname{short}\) 表示最短, \(\operatorname{long}\) 表示最长。

引理 \(6\)

所有后缀链接构成一棵根节点为 \(t_0\) 的树。

这是(@ZTer)大佬博客里的一张图。

构造

在构造之前我们先定义一些记号。

对于每一个结点 \(v\) ,它对应一个等价类 \(E\) 。我们记 \(\operatorname{long}(v)\)\(E\) 中最长的串,\(\operatorname{len}(v)\) 为其长度;\(\operatorname{short}(v)\)\(E\) 中最短的串,\(\operatorname{minlen}(v)\) 为其长度。那么 \(E\) 中每个串都是 \(\operatorname{long}(v)\) 的后缀,且所有字符串的长度的并集为 \({\operatorname{minlen}(v)≤x≤\operatorname{len}(v),x∈N^{*}}\)

其中 \(\operatorname{minlen}(v) = \operatorname{len}(\operatorname{link}(v)) + 1\)

同时根据上述引理 \(3\)\(4.2\),我们可以得出从任意结点 \(v_0\) 顺着后缀链接遍历,总可以访问到 \(P\)。途中会访问到若干结点 \(v_i\)\(v_i\) 中包含的字符串的长度恰好覆盖一段区间 \([\operatorname{minlen}(v_i),\operatorname{len}(v_i)]\) 的每一个整数,并且每一个 \(v_i\) 恰好覆盖的区间都不相交,这些区间的并为 \({0≤x≤\operatorname{len}(v_0),x∈N^{*}}\)

接下来是构造的算法流程:

SAM 中的每个点需要维护一个 \(len\) 和一个 \(link\),以及出边。

最初 SAM 中只有一个状态 \(t_0\),编号为 \(0\),其中 \(t_0\)\(len = 0, link = -1\)。令 \(last\) 为当前已经做完的字符串 \(S\) 所对应的结点,最初 \(last = 0\),现在我们要添加一个字符 \(c\).

  1. 创建一个新状态 \(cur\),则 \(\operatorname{len}(cur) = \operatorname{len}(last) + 1\).

  2. \(last\) 开始跳后缀链接,如果当前跳到的点 \(v\) 没有出边 \(c\) 就创建一条 \((v, cur)\) 的边 \(c\).

  3. 如果走到了 \(t_0\),那么 \(\operatorname{link}(cur) = 0\),去 \(7\).

  4. 如果当前结点 \(v\) 有出边 \(c\),令该结点为 \(p\),它沿出边 \(c\) 到达 \(q\),然后分类讨论。

  5. 如果 \(\operatorname{len}(q) = \operatorname{len}(p) + 1\),赋值 \(\operatorname{link}(cur) = q\),转 \(7\).

  6. 否则复制状态 \(q\) 到新点 \(copy\)(信息包括 \(\operatorname{link}(q)\)\(q\) 的出边),然后 \(\operatorname{len}(copy) = \operatorname{len}(p) + 1\)\(\operatorname{link}(q) = copy, \operatorname{link}(cur) = copy\)。接着从 \(p\) 出发遍历后缀链接,若遍历到有出边 \(c\) 的点 \(v\) 就重定向这条边为 \((v, copy)\)

  7. \(last = cur\),结束

除了分类讨论其实都比较好懂。

\(1\)\(2\) 部分是因为 \(len(last) = S\),所以 \(len(S + c) = len(last) + 1\)。同时所有 \(S\) 的后缀都可以添加一个 \(c\) 成为 \(S + c\) 的后缀,从而我们就达到记录每一个后缀的目的。

\(3\) 部分也是显然的,重点来讲 \(4, 5, 6\) 部分。

我们考虑 \(long(p)\)\(S\) 的后缀,所以 \(long(p) + c\) 一定是 \(S + c\) 的后缀,又因为从 \(S\)\(link\) 时所经过的点的 \(\operatorname{len}(p)\) 长度递减,所以对于 \(cur\) 来说 \(\operatorname{long}(p) + c\) 满足“最长、不等价、后缀”的后缀链接定义。所以 \(\operatorname{long}(p) + c\) 就应该是 \(\operatorname{link}(cur)\) 中的最长字符串。

但我们还不知道 \(\operatorname{long}(p) + c\) 是否是 \(q\) 中最长的字符串,所以要分两类讨论。

\(\operatorname{len}(q) = \operatorname{len}(p) + 1\) 时就很显然了,我们考虑另一种情况。

由于 \(q\) 中最长的字符串不是 \(\operatorname{long}(p) + c\),所以我们需要一个 \(\operatorname{long}(p) + c\) 为最长字符串的等价类。于是就有了 \(copy\)\(\operatorname{link}(cur) = copy\)

然后我们考虑新拆分出来的 \(copy\) 实际上是从一个包含字符串的长度可以覆盖区间 \([\operatorname{minlen}(q), \operatorname{len}(q)]\) 中每一个整数的等价类 \(q\) 中拆分出一个包含字符串的长度覆盖区间 \([\operatorname{minlen}(q), \operatorname{len}(p) + 1]\) 的等价类。所以分裂之后的新 \(q\) 就应该覆盖 \((\operatorname{len}(p) + 1, \operatorname{len}(q)]\)

接着由于 \(\operatorname{len}(\operatorname{link}(q)) = \operatorname{minlen}(q) - 1\),且 \(\operatorname{len}(\operatorname{link}(copy)) = \operatorname{minlen}(q) - 1\),所以 \(\operatorname{link}(copy) = \operatorname{link(q)}\)。同时 \(\operatorname{minlen}(q) - 1 < \operatorname{len}(p) + 1\),所以 \(\operatorname{long}(copy)\) 才是 \(\operatorname{long}(q)\) 的“最长、不等价、后缀”即后缀链接,所以 \(\operatorname{link}(q) = copy\)

除此之外,由于原先的 \(q\) 变为了新 \(q\),所以还需要重定向一些边,显然只有 \(\operatorname{long}(p)\) 的后缀加上 \(c\) 才会连向 \(copy\)

时间和空间

时间复杂度为 \(O(nlog|\sum|)\),因为要用 \(map\) 维护出边。如果字符集大小足够小可以直接使用 \(int\) 就是线性的了。

SAM 的结点数不会超过 \(2n - 1\),边数不会超过 \(3n - 4\) 所以要注意数组大小。

模板

namespace SAM {
    struct state {
        int len, link, f;
        map<char, int> ne;
    } st[N << 1];
    int tot, last;
    vector<int> g[N << 1];

    void init() {
        st[0].len = 0, st[0].link = -1;
        tot = 0, last = 0;
    }

    void extend(char c) {
        int cur = ++ tot;
        st[cur].len = st[last].len + 1;
        st[cur].f = 1;
        int p = last;
        while (p != -1 && !st[p].ne.count(c)) {
            st[p].ne[c] = cur;
            p = st[p].link;
        } 
        if (p == -1) {
            st[cur].link = 0;
        } else {
            int q = st[p].ne[c];
            if (st[q].len == st[p].len + 1) {
                st[cur].link = q;
            } else {
                int clone = ++ tot;
                st[clone].len = st[p].len + 1;
                st[clone].link = st[q].link;
                st[clone].ne = st[q].ne;
                while (p != -1 && st[p].ne[c] == q) {
                    st[p].ne[c] = clone;
                    p = st[p].link;
                }
                st[q].link = st[cur].link = clone;
            }
        }
        last = cur;
    }
}; 
posted @ 2025-03-07 16:11  比翼の鼠  阅读(81)  评论(0)    收藏  举报