SAM 做题笔记

这里是 SAM 感性瞎扯。


最近学了后缀自动机(Suffix_Automaton,SAM),深感其巧妙之处,故写文以记之。

部分文字与图片来源于 OI-Wiki,hihoCoder 与一些个人博客,链接在文章最底端。

在此之前请先了解有关于 SAM 的所有函数,并最好理解 OI-Wiki 上有关于 SAM 的五条引理(链接在最底部,引理在下文中有提到)。


一些重要的定义与引理:

  • \(T\):初始状态。
  • \(\mathrm{endpos}(i)\):字符串 \(i\) 在 \(s\) 中所有出现的结束位置集合。例如当 \(s=\texttt{"abcab"}\) 时,\(\mathrm{endpos}(\texttt{"ab"})=\{2,5\}\),因为 \(s[1:2]=s[4:5]=\texttt{"ab"}\)。
  • \(\mathrm{substr}(i)\):状态(SAM 上的一个节点)\(i\) 所表示的所有子串的集合。
  • \(\mathrm{shortest}(i)\):状态 \(i\) 所表示的所有子串中,长度最短的那一个子串
  • \(\mathrm{longest}(i)\):状态 \(i\) 所表示的所有子串中,长度最长的那一个子串
  • \(\mathrm{minlen}(i)\):状态 \(i\) 所表示的所有子串中,长度最短的那一个子串的长度。即 \(\mathrm{minlen}(i)=|\mathrm{shortest}(i)|\)。
  • \(\mathrm{len}(i)\):状态 \(i\) 所表示的所有子串中,长度最长的那一个子串的长度。即 \(\mathrm{len}(i)=|\mathrm{longest}(i)|\)。
  • \(\mathrm{link}(i)\):\(\mathrm{longest}(i)\) 最长的一个后缀 \(w\ (w\notin \mathrm{substr}(i))\) 所在的状态。换句话说,一个后缀链接 \(\mathrm{link}(i)\) 连接到对应于 \(\mathrm{longest}(i)\) 的最长后缀的另一个 \(\mathrm{endpos}\) 等价类的状态。有 \(\mathrm{minlen}(i)=\mathrm{len(link}(i))+1\)。

引理 1: 字符串 \(s\) 的两个非空子串 \(u\) 和 \(w\)(假设 \(|u|\leq |w|\))的 \(\mathrm{endpos}\) 相同,当且仅当字符串 \(u\) 在 \(s\) 中的每次出现,都以 \(w\) 后缀的形式存在。证明详见 OI-Wiki,下(引理 2~5)同

引理 2:考虑两个非空子串 \(u\) 和 \(w\)(假设 \(|u|\leq |w|\))。要么 \(\mathrm{endpos}(u)\cup \mathrm{endpos}(w)=\varnothing\),要么 \(\mathrm{endpos}(u)\subseteq\mathrm{endpos}(w)\),取决于 \(u\) 是否为 \(w\) 的一个后缀:

\[\begin{cases}\mathrm{endpos}(u)\subseteq\mathrm{endpos}(w)\quad &\mathrm{if}\ u\ \mathrm{is\ a\ suffix\ of}\ w\\\mathrm{endpos}(u)\cup \mathrm{endpos}(w)=\varnothing&\mathrm{otherwise}\end{cases}
\]

引理 3:考虑一个 \(\mathrm{endpos}\) 等价类,将其中所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者总为为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 \([\mathrm{minlen,len}]\)(即排序后长度连续且不等)。

引理 4:所有后缀链接构成一棵根节点为 \(T\) 的树。

  • 定义后缀路径 \(p\to q\) 表示在后缀链接构成的树中 \(p\to q\) 的路径。

引理 5:通过 \(\mathrm{endpos}\) 集合构造的树(每个子节点的 \(subset\) 都包含在父节点的 \(subset\) 中)与通过后缀链接 \(\mathrm{link}\) 构造的树相同。

(图片来源于 OI-Wiki)

引理 6:对于一个状态 \(t\),\(\mathrm{substr}(t)\) 中所有子串后面接上同一个字符 \(c\) 之后,新的子串仍然都属于同一个状态(如果该状态存在)。

假设存在两个字符串 \(s_p,s_{p'}\in \mathrm{substr}(t)\) 满足 \(\mathrm{endpos}(s_p+c)\neq \mathrm{endpos}(s_{p'}+c)\)。不妨设 \(\mathrm{endpos}(s_{p'}+c)\) 包含 \(\mathrm{endpos}(s_p+c)\) 所没有的一个位置 \(pos\),那么 \(\mathrm{endpos}(s_{p'})\) 中必定含有 \(pos-1\),而 \(\mathrm{endpos}(s_p)\) 中必定没有。但是它们属于同一个状态,矛盾。得证。


一些重要的结论:(个人证明,并不非常严谨!)

结论 0:如果我们从任意状态 \(t_0\) 开始顺着后缀链接遍历,总会到达初始状态 \(T\)。这种情况下我们可以得到一个互不相交的区间 \([\mathrm{minlen}(t_i),\mathrm{len}(t_i)]\) 的序列,且它们的并集形成了连续的区间 \([0,\mathrm{len}(t_0)]\)。

该引理为 OI-Wiki 在 “小结” 最后一条列出的性质,根据后缀链接的性质和引理 3,4 易证。

结论 1:从表示 \(s[1:i]\) 的状态 \(t_0\) 不断跳后缀链接 \(\mathrm{link}\) 直到初始节点 \(T\),其遍历到的所有状态 \(t_0,t_1,t_2,\cdots,T\) 所包含的子串集合刚好为所有 \(s[1:i]\) 的后缀所表示的状态,即 \(\mathrm{substr}(t_0)\cup\mathrm{substr}(t_1)\cup\mathrm{substr}(t_2)\cup\cdots\cup\mathrm{substr}(T)=\{s[x:i]\ (1\leq x\leq i)\}\)。换句话说,如果你在 \(\mathrm{SAM}_i\) 上跑所有 \(s[1:i]\) 的后缀,那么跑到的所有状态都在后缀路径 \(t_0\to T\) 上

证明 1:由后缀链接 \(\mathrm{link}\) 的定义,对于任意一个状态 \(t_0\),都有 \(x\) 是 \(y\) 的真后缀,其中 \(x\in\mathrm{substr(link}(t_0)),y\in\mathrm{substr}(t_0)\)。那么 \(S=\mathrm{substr}(t_0)\cup\mathrm{substr}(t_1)\cup\cdots\cup\mathrm{substr}(T)\) 中的所有子串(这里的子串是相对于 \(s[1:i]\) 而不是所包含的字符串的子串)都是 \(\mathrm{longest(t_0)}\) 即 \(s[1:i]\) 的后缀。又根据结论 0,\(S\) 中所有字符串长度覆盖了区间 \([0,\mathrm{len}(t_0)]\),即 \([0,i]\)。得证。

结论 2:一个状态 \(t\) 所表示的所有子串 \(\mathrm{substr}(t)\),等于初始状态 \(T\) 到该状态上所有路径所形成的所有字符串。如果添加一条转移边 \((p,q)\),字符为 \(c\),那么相当于将 \(\mathrm{substr}(p)\) 中所有子串末尾加上字符 \(c\) 添加到 \(\mathrm{substr}(q)\) 里面。

可以结合引理 6 与结论 1 感性理解(其实是我不太会证明,但是它满足这个性质)。如果想要直观理解可以看下文 Case 2 中的插图。

结论 3:考虑存在一个转移 \((p,q)\) 且 \(\mathrm{len}(p)+1=\mathrm{len}(q)\),那么其他所有指向 \(q\) 的转移 \((p_i,q)\) 的 \(p_i\) 都在后缀路径 \(p\to T\) 上。

证明 3:首先,根据结论 2,我们知道如果存在转移 \((p,q)\),字符为 \(c\),那么一定有 \(\mathrm{len}(p)+1\leq\mathrm{len}(q)\)。因此,假设有另外一个状态 \(p'\) 存在转移 \((p',q)\) 且 \(p'\) 不在后缀路径 \(p\to T\) 上(显然 \(p'\) 不能为 \(p\)),那么有 \(\mathrm{len}(p')< \mathrm{len}(p)\)。设 \(s_{p}=\mathrm{longest}(p),s_{p'}=\mathrm{longest}(p')\)。因为 \(s_p+c\) 与 \(s_{p'}+c\) 同属于 \(\mathrm{substr}(q)\),且 \(|s_{p'}+c|<|s_p+c|\),所以根据引理 3,\(s_{p'}+c\) 是 \(s_p+c\) 的后缀。所以 \(s_{p'}\) 是 \(s_p\) 的后缀。因此,根据引理 2,\(\mathrm{endpos}(s_p)\subsetneq \mathrm{endpos}(s_{p'})\ (p\neq p')\)。因此,根据引理 5,\(p'\) 在 \(\mathrm{link}\) 树上一定为 \(p\) 的父节点。这与假设矛盾。得证。

结合上图以更好理解(图片来源于 hihoCoder)。

推论 3:指向状态 \(q\) 的转移 \((p_i,q)\) 的所有状态 \(p_i\),在 \(\mathrm{link}\) 树上一定为一条深度递减的链 \(p_0\to p_x\),且有 \(\mathrm{minlen}(p_x)+1=\mathrm{minlen}(q),\mathrm{len}(p_0)+1=\mathrm{len}(q)\)。


SAM 的构造方式:

类似数学归纳法:假设已经构造好了 \(\mathrm{SAM}_{i-1}\),其状态为 \(las\),这样所需要做的就是添加一个字符 \(s_i\)。

首先,\(\mathrm{SAM}_{i-1}\) 是无法表示 \(s[1:i]\) 的(因为它只接受 \(s[1:i-1]\) 的子串),所以我们新建一个节点 \(cur\) 表示至少包含 \(s[1:i]\) 的状态,显然有 \(\mathrm{len}(cur)=i\)(或者说等于 \(\mathrm{len}(las)+1\))。可以发现 \(\mathrm{endpos}(s[1:i])=\{i\}\),因为 \(s[1:i]\in\mathrm{substr}(cur)\),而 \(s[1:i]\) 在 \(s[1:i]\) 中显然只以 \(i\) 为结束位置出现。

  • 为什么说至少包含?因为 \(s[1:i]\) 的其它后缀也可能只以 \(i\) 为结束位置出现。例如 \(s=\texttt{aab}\)(假设 \(i=3\)),那么不仅是 \(\mathrm{endpos}(\texttt{"aab"})=\{3\}\),\(\texttt{"ab"}\) 与 \(\texttt{"b"}\) 的 \(\mathrm{endpos}\) 集合也为 \(\{3\}\)。

Case 1:根据引理 1 与 2,如果不考虑重复状态,那我们只需要将后缀路径 \(las\to T\) 上的所有状态往 \(cur\) 连一条字符为 \(s_i\) 的转移边,并将 \(\mathrm{link}(cur)\) 设为 \(T\) 即可(因为后缀路径 \(las\to T\) 上的所有点刚好表示了 \(s[1:i-1]\) 的所有后缀,在其后面添加字符 \(s_i\) 就可以表示 \(s[1:i]\) 的所有后缀)。这其实是 Case 1,即后缀路径 \(las\to t\) 上所有状态都没有字符 \(s_i\) 的转移边。容易发现这种情况仅在 \(s_i\) 未在 \(s[1:i-1]\) 中出现过时发生。

如果有重复怎么办?我们从 \(las\) 开始跳到的不重复的所有状态往 \(cur\) 连一条 \(s_i\) 的转移边,并设遇到的第一个重复的转移为 \((p,q)\)。为什么不能再添加 \((p,cur)\) 的 \(s_i\) 的转移了?因为这样会导致从 \(T\to q\) 和 \(T\to cur\) 可以表示相同的子串,破坏了 SAM 的性质。记在后缀路径 \(las\to T\) 跳到 \(p\) 的上一个状态为 \(p'\)(即 \(\mathrm{link}(p')=p\))。显然 \((p',cur)\) 有一条字符为 \(s_i\) 的转移。

此时 \(cur\) 的后缀链接应该怎么连?我们想要满足 \(\mathrm{len(link(}cur))+1=\mathrm{minlen}(cur)\)。这个 \(\mathrm{minlen}(cur)\) 实际上就是 \(\mathrm{minlen}(p')+1\),那么 \(\mathrm{len}(p)=\mathrm{minlen}(p')-1=\mathrm{len(link(}cur))-1\)(因为 \(\mathrm{link}(p')=p\))。

Case 2:\(\mathrm{len}(q)=\mathrm{len}(p)+1\),此时 \(q\) 中包含的最长子串就是 \(p\) 中的最长子串接上字符 \(s_i\)。那么只需将 \(\mathrm{link}(cur)\gets q\) 即可。因为 \(\mathrm{len}(q)=\mathrm{len}(p)+1=\mathrm{len(link(}cur))\),刚好是我们想要的。

(图片来源于 hihocoder)

上图中,在添加 \(s_5=\texttt{a}\) 时,从 \(T(S)\to 1\) 已经有了现成的字符 \(s_5\) 的转移。此时 \(p=T,q=1\)。因为 \(\mathrm{len}(1)=\mathrm{len}(T)+1\ (1=0+1)\),所以直接将 \(\mathrm{link}(6)\gets 1\) 即可。

注意上图中状态 \(4,5,6\) 所表示的子串,可以发现状态 \(6\) 所表示的子串是状态 \(4,5\) 所表示的所有子串后接上字符 \(s_5\) 得到的。这很好地验证了结论 2,也非常直观形象地阐释了 Case 2 的情况。

Case 3:\(\mathrm{len}(q)>\mathrm{len}(p)+1\),此时 \(q\) 对应了 \(s[1:i-1]\) 的更长的子串(感觉 OI-Wiki 这里讲得不是很明白)。此时除了把 \(q\) 拆开来别无他法。具体来说,将 \(q\) 所表示的所有长度不大于 \(\mathrm{len}(p)+1\) 的子串提出来,丢给一个新建的状态 \(q'\),然后将 \(\mathrm{link(cur)}\gets q'\),同时添加转移 \((q',cur)\),也就是说我们凭空创造了一个满足 Case 2 的状态 \(q\)

显然,无中生有是要付出一些时间代价的。先考虑 \(q\) 和 \(q'\) 的内部情况。具体的,\(\mathrm{link}(q)\) 应改为 \(q'\),而 \(\mathrm{link}(q')\) 应该继承原来的 \(\mathrm{link}(q)\)。此外,一些本来转移到 \(q\) 的转移 \((p_i,q)\) 也应该变为 \((p_i,q')\)。具体地,我们从后缀路径 \(p\to T\) 继续往上跳,沿路径跳到的所有状态 \(p_i\),如果有一条 \((p_i,q)\) 的转移,那么将其改为 \((p_i,q')\)。

(图片来源于 hihocoder)

特别的,如果跳到一个状态 \(P\),它没有 \((P,q)\) 的转移,此时退出即可,因为根据结论 3,此时再往上跳也不可能出现另外一个状态 \(P'\) 有 \((P',q)\) 的转移。换句话说,我们找到后缀路径 \(p\to T\) 上从 \(p\) 开始的一段有 \((p_i,q)\) 转移的所有状态,并将其修改为 \((p_i,q')\)。因为根据推论 3,路径上有且仅有一段状态 \(p_i\) 有 \((p_i,q)\) 的转移,且所有 \(p_i\) 所表示的子串加上字符 \(s_i\),刚好能表示状态 \(q\) 原本所表示的所有长度不大于 \(\mathrm{len(p)}+1\) 的子串,如上图。

(图片来源于 hihocoder)

上图中,我们把 \(q=3\) 的不大于 \(\mathrm{len}(T)+1=1\) 的所有子串提出来,丢给一个新建的状态 \(q'=5\),然后 \(\mathrm{link}(4\ (cur))\gets 5\ (q')\) 并添加状态 \((5,4)\)(即 \((q',cur)\))。内部,\(\mathrm{link}(3\ (q))\gets 5\ (q')\),同时 \(\mathrm{link}(5\ (q')) \gets T\)(原来的 \(\mathrm{link}(3)\))。最后从 \(T\ (p)\) 往上跳后缀连接直到不存在连向 \(3\) 的路径或到了初始状态 \(T\)(当然,这里的例子只有 \(T\) 一个点,不过我们需要知道并不一定会跳到 \(T\),因为有可能跳到中间的某个状态 \(P\) 时就没有转移 \((P,3\ (q))\) 了),并将所有连向 \(3\ (q)\) 的转移连向 \(5\ (q')\),即 \((T,3)\) 变为了 \((T,5)\)。

综合上述三种情况,我们就可以在线性时间内建造出一个字符串 \(s\) 的 SAM(关于其时间复杂度为线性的证明,详见 OI-Wiki)。

部分额外信息在代码与应用中有提到。


代码与应用:

请移步 SAM 做题笔记


一些资料:

如发现错误或有不理解的地方可以在下方评论区留言,我会尽快修改。

SAM 感性瞎扯的更多相关文章

  1. SAM 做题笔记(各种技巧,持续更新,SA)

    SAM 感性瞎扯. 这里是 SAM 做题笔记. 本来是在一篇随笔里面,然后 Latex 太多加载不过来就分成了两篇. 标 * 的是推荐一做的题目. trick 是我总结的技巧. I. P3804 [模 ...

  2. 【算法专题】后缀自动机SAM

    后缀自动机是用于识别子串的自动机. 学习推荐:陈立杰讲稿,本文记录重点部分和感性理解(论文语言比较严格). 刷题推荐:[后缀自动机初探],题目都来自BZOJ. [Right集合] 后缀自动机真正优于后 ...

  3. 后缀自动机SAM

    某神犇:"初三还不会后缀自动机,那就退役吧!" 听到这句话后,我的内心是崩溃的. 我还年轻,我还不想退役--于是,我在后来,努力地学习后缀自动机. 终于,赶在初三开学前,我终于学会 ...

  4. SAM[详细~bushi]

    基础性质概念 后缀自动机:S的SAM是个DAG,每个节点叫状态,每条带字符ch边表示+ch转移,从开始节点往下,任何一条路径都会对应一个S的子串. 不过为什么要叫"后缀"自动机呢? ...

  5. SAM复杂度证明

    关于$SAM$的复杂度证明(大部分是对博客的我自己的理解和看法) 这部分是我的回忆,可省略 先回忆一下$SAM$ 我所理解的$SAM$,首先扒一张图 初始串$aabbabd$ 首先发现,下图里的$S- ...

  6. 后缀自动机(SAM)+广义后缀自动机(GSA)

    经过一顿操作之后竟然疑似没退役0 0 你是XCPC选手吗?我觉得我是! 稍微补一点之前丢给队友的知识吧,除了数论以外都可以看看,为Dhaka和新队伍做点准备... 不错的零基础教程见 IO WIKI ...

  7. SAM初探

    SAM,即Suffix Automaton,后缀自动机. 关于字符串有很多玩法,有很多算法都是围绕字符串展开的.为什么?我的理解是:相较于数字组成的序列,字母组成的序列中每个单位上元素的个数是有限的. ...

  8. bzoj4199:NOI2015D2T2品酒大会(SAM版)

    SAM感觉写起来比SA更直观(?) #include <iostream> #include <cstdio> #include <cstring> #includ ...

  9. SAM/BAM文件处理

    当测序得到的fastq文件map到基因组之后,我们通常会得到一个sam或者bam为扩展名的文件.SAM的全称是sequence alignment/map format.而BAM就是SAM的二进制文件 ...

随机推荐

  1. CentOS 压缩解压

    目录 命令 tar gzip.gunzip bzip2.bunzip2 zip.unzip 命令组合 打包:将多个文件合成一个总的文件,这个总的文件通常称为"归档". 压缩:将一个 ...

  2. 重学c#系列——list(十二)

    前言 简单介绍一下list. 正文 这里以list为介绍. private static readonly T[] s_emptyArray = new T[0]; public List() { t ...

  3. [no code][scrum meeting] Beta 5

    $( "#cnblogs_post_body" ).catalog() 例会时间:5月18日14:30,主持者:叶开辉 下次例会时间:5月19日11:30,主持者:黎正宇 一.工作 ...

  4. Python课程笔记(七)

    今天学习神奇的海龟,非常有意思,还有很多图片想去绘制,分享一个turtle绘图网站: https://www.python123.io/index/turtles/latest , 要是可以分享出源码 ...

  5. Apache Kafka 学习笔记

    1. 介绍Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写.Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据. 这种动 ...

  6. CSP-S 2021 退役记

    写的比较草率,但的确是真实感受. 10.23 回寝室前敲了一个 dinic 板子,觉得不会考... 10.24 8:00 起床,还好今天宿管不在,可以起的晚一点. 吃了早饭来机房颓废. 10:00 似 ...

  7. sed初理多行合并+sed之G、H、g、h使用+sed n/N使用说明

    转载:[shell]sed处理多行合并 - seyjs - 博客园 (cnblogs.com) 文件格式 table=t1 name owner address table=t2 id text co ...

  8. Cobar SQL审计的设计与实现

    背景介绍 Cobar简介 Cobar 是阿里开源的一款数据库中间件产品. 在业务高速增长的情况下,数据库往往成为整个业务系统的瓶颈,数据库中间件的出现就是为了解决数据库瓶颈而产生的一种中间层产品. 在 ...

  9. Kali-Linux 2020如何设置中文

    话不多说,直接上步骤 首先,想要修改系统默认语言普通用户是办不到的,这个时候就需要切换为root用户在终端输入 sudo su(切换用户指令,后面不加用户名就默认切换为root) 输入管理员密码后就像 ...

  10. Java8新特性之方法引用&Stream流

    Java8新特性 方法引用 前言 什么是函数式接口 只包含一个抽象方法的接口,称为函数式接口. 可以通过 Lambda 表达式来创建该接口的对象.(若 Lambda 表达式抛出一个受检异常(即:非运行 ...