开个坑,待补
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$.
- 创建一个新状态 $cur$,则 $\operatorname{len}(cur) = \operatorname{len}(last) + 1$.
-
从 $last$ 开始跳后缀链接,如果当前跳到的点 $v$ 没有出边 $c$ 就创建一条 $(v, cur)$ 的边 $c$.
-
如果走到了 $t_0$,那么 $\operatorname{link}(cur) = 0$,去 $7$.
- 如果当前结点 $v$ 有出边 $c$,令该结点为 $p$,它沿出边 $c$ 到达 $q$,然后分类讨论。
- 如果 $\operatorname{len}(q) = \operatorname{len}(p) + 1$,赋值 $\operatorname{link}(cur) = q$,转 $7$.
- 否则复制状态 $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)$。
- $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;
}
};