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\) 表示最长子串长度。

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 手残+强迫症

posted @ 2022-08-02 17:34  Schucking_Sattin  阅读(478)  评论(0编辑  收藏  举报