SAM (后缀自动机)
SAM
Intro
Use
Definition
字符串 \(s\) 的 SAM 是一个接受 \(s\) 的所有后缀的最小 DFA (确定性有限自动机或确定性有限状态自动机)。
-
SAM 是一张 DAG。节点被称为 状态,边被称作状态间的 转移。
-
图存在一个源点 \(t_0\),称作 初始状态,其它各节点均可从 \(t_0\) 出发到达。
-
每个 转移 都标有一些字母。从一个节点出发的 转移 均不同。
-
存在一个或多个 终止状态。如果我们从 \(t_0\) 出发,最终转移到了一个终止状态,则路径上的所有转移连接起来一定是 \(s\) 的一个后缀。
\(s\) 的每个后缀均可用一条从 \(t_0\) 到某个终止状态的路径构成。
-
在所有满足上述条件的自动机中,SAM 的结点数是最少的。
Demonstration
endpos (结束位置)
Definition
endpos
对于 \(s\) 的任意非空子串 \(t\),我们记 \(\operatorname{endpos}(t)\) 为在 \(s\) 中 \(t\) 的所有结束位置。
规定字符串从 \(0\) 开始编号。
\(\operatorname{endpos}()\) 是一个集合。
等价类
两个子串 \(t_1\) 和 \(t_2\) 的 \(\operatorname{endpos}\) 集合可能相等:\(\operatorname{endpos}(t_1)=\operatorname{endpos}(t_2)\)。
这样所有 \(s\) 的非空子串都可以根据它们的 \(\operatorname{endpos}\) 被分成若干 等价类。
Property
Lemma 1
Lemma 1 (endpos)
字符串 \(s\) 的两个非空子串 \(u\) 和 \(w\) (假设 \(|u|\le |w|\))的 \(\operatorname{endpos}\) 相同,当且仅当字符串 \(u\) 在 \(s\) 中的每次出现,都是以 \(w\) 的后缀的形式存在的。
Lemma 2
Lemma 2 (endpos)
考虑两个非空子串 \(u\) 和 \(w\)(假设 \(|u|\le |w|\))。则:
\[\begin{cases} \operatorname{endpos}(w)\subseteq \operatorname{endpos}(u) & \text{if $u$ is a suffix of $w$} \\ \operatorname{endpos}(w)\cap \operatorname{endpos}(u)=\emptyset & \text{otherwise} \end{cases} \]
Lemma 3
Lemma 3 (endpos)
考虑一个 \(\operatorname{endpos}\) 等价类,将类中所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 \([x,y]\)。
其中 \(x\) 表示最短子串长度,\(y\) 表示最长子串长度。
Link (后缀链接)
Intro
考察 SAM 中某个不是 \(t_0\) 的状态 \(v\)。
\(v\) 对应着 一个 等价类,设 \(w\) 是这个等价类中长度最长的子串。
将 \(w\) 的后缀按长度降序排列,则前面一些后缀和 \(w\) 同属一个等价类,而另一些后缀对应另外的 \(\operatorname{endpos}\)。
并且至少有一个 \(w\) 的后缀对应另外的 \(\operatorname{endpos}\)(考虑空串)。
设 \(x\) 为满足不属于在包含 \(w\) 的等价类里的一个 \(w\) 的后缀,则状态 \(v\) 连向状态 \(x\)。
状态的本质就是重要的转折子串。
Property
Lemma 4
Lemma 4 (link)
所有后缀链接构成一棵根节点为 \(t_0\) 的树。
Lemma 5
Lemma 5 (link)
通过 \(\operatorname{endpos}\) 集合构造的树(每个子节点的子集都包含在父节点的子集中)与通过后缀链接构成的树相同。
Demonstration
Algorithm
SAM 是一个 在线 算法。
大致方法是:逐个加入 字符,对应维护 SAM。
一开始 SAM 只包含一个状态 \(t_0\),编号为 \(0\)(其它状态的编号为 \(1,2,\cdots\))。为了方便,对于状态 \(t_0\) 我们指定 \(len=0\)、\(link=-1\)(\(-1\) 表示虚拟状态)。
现在,任务转化为实现给当前字符串添加一个字符 \(c\) 的过程。算法流程如下:
- 令 \(last\) 为添加字符 \(c\) 之前,整个字符串对应的状态(一开始我们设 \(last=0\),算法的最后一步更新 \(last\))。
- 创建一个新的状态 \(cur\),并将 \(len(cur)\) 赋值为 \(len(last)+1\),在这时 \(link(cur)\) 的值还未知。
- 现在我们按以下流程进行(从状态 \(last\) 开始)。如果还没有到字符 \(c\) 的转移,我们就添加一个到状态 \(cur\) 的转移,遍历后缀链接。如果在某个点已经存在到字符 \(c\) 的转移,我们就停下来,并将这个状态标记为 \(p\)。
- 如果没有找到这样的状态 \(p\),我们就到达了虚拟状态 \(t_0\),我们将 \(link(cur)\) 赋值为 \(0\) 并退出。
- 假设现在我们找到了一个状态 \(p\),其可以通过字符 \(c\) 转移。我们将转移到的状态标记为 \(q\)。
- 现在我们分类讨论两种状态,要么 \(len(p)+1=len(q)\),要么不是。
- 如果 \(len(p)+1=len(q)\),我们只要将 \(link(cur)\) 赋值为 \(q\) 并退出。
- 否则就会有些复杂。需要 复制 状态 \(q\):我们创建一个新的状态 \(clone\),复制 \(q\) 的除了 \(len\) 的值以外的所有信息(后缀链接和转移)。我们将\(len(clone)\) 赋值为 \(len(p)+1\)。
复制之后,我们将后缀链接从 \(cur\) 指向 \(clone\),也从 \(q\) 指向 \(clone\)。
最终我们需要使用后缀链接从状态 \(p\) 往回走,只要存在一条通过 \(p\) 到状态 \(q\) 的转移,就将该转移 重定向 到状态 \(clone\)。 - 以上三种情况,在完成这个过程之后,我们将 \(last\) 的值更新为状态 \(cur\)。
上面是贺的 OI-wiki 的讲解。没有图怎么行呢?!
Demonstration
之前那些图是贺的,接下来我亲自画。假设要对 \(s='\#bcbc'\) 建立 SAM。
下面这张图有点小问题:3 号节点有两个后缀链接。
就是对于 \(q\) 这样本身就有后缀链接的节点,在重定向时要把原来的后缀链接去掉。
实际操作很简单嘛,就是赋值。
最后是完整的 SAM:
一个下午.past. QWQ 手残+强迫症