SAM 基础
SAM 的定义
-
SAM 是一张有向无环图。结点被称作 状态 ,边被称作状态间的转移
-
图存在一个源点
,称作 初始状态,其它各结点均可从 出发到达 -
每个 转移 都标有一些字母。从一个结点出发的所有转移均不同
-
存在一个或多个 终止状态 。如果我们从初始状态
出发,最终转移到了一个终止状态,则路径上所有转移连接起来一定是字符串 的一个后缀。 的每个后缀均可用一条从 到某个终止状态的路径构成 -
在所有满足上述条件的自动机中,SAM 的结点数最少
子串的性质
-
SAM 包含关于字符串
的所有子串的信息。任意从初始状态 开始的路径,如果我们将转移路径上的标号写下来,都会形成 的一个子串。反之,每个 的子串对应从 开始的某条路径。 -
到达某个状态的路径可能不止一条,因此我们说一个状态对应一些字符串的集合,这个集合的每个元素对应这些路径
结束位置 endpos
-
考虑字符串
的任意非空子串 ,我们记 为在字符串中 的所有结束位置 -
两个子串的
集合可能相等,这样所有字符串 的非空子串都可以根据它们的 集合被分为若干 等价类 -
对于 SAM 中的每个状态对应一个或多个
相同的子串,也就是SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。SAM 的状态个数等价于 相同的一个或多个子串所组成的集合的个数 + 1
一些引理
-
字符串
的两个非空子串 和 (假设 )的 相同,当且仅当字符串 在 中的每次出现,都是以 的后缀形式存在的 -
考虑两个非空子串
和 ( )。那么要么 和 不相交,要么 是 的子集,这取决于 是否是 的一个后缀 -
如果集合
与 有至少一个公共元素,那么 是 的子集 -
考虑一个
等价类,将类中所有子串按长度非递增顺序排序。每个子串都不会比它前一个子串长,且一定是前一个子串的后缀。也就是对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中中的子串长度恰好覆盖一个区间
后缀链接 link
-
考虑一个非初始状态的状态
。我们已经知道状态 对应于具有相同 的等价类。我们定义 为这些字符串中最长的一个,则所有其它字符串都是 的后缀 -
我们还知道字符串
的前几个后缀(按长度降序考虑)全部包含于这个等价类,且所有其它后缀(至少有一个--空后缀)在其它的等价类中,我们记 为最长的这样的后缀,然后将 的后缀链接接连奥 上 -
换句话说,一个后缀链接
链接到对应 的最长后缀是另一个 等价类的状态 -
以下我们假设初始状态
对应它自己这个等价类 (只包含一个空字符串)。为了方便,我们规定
一些引理
-
所有后缀链接构成一颗根节点为
的树 -
通过
集合构造的树(每个子节点的 都包含在父节点的 中)与通过后缀链接 构造的树相同
小结
-
的子串可以根据它们结束的位置 被划分为多个等价类 -
SAM 由初始状态
和与每一个 等价类对应的每个状态组成 -
对于每一个状态
,一个或多个子串与之匹配。我们记 为其中最长的一个字符串,记 为它的长度。类似地,记 为最短的子串,它的长度为 。那么对应这个状态的所有字符串都是字符串 的不同后缀,且所有字符串的长度恰好覆盖区间 中的每一个整数 -
对于任意不是
以外的状态 ,定义后缀链接为连接到对应字符串 的长度为 的后缀的一条边。从根节点 出发的后缀链接可以形成一棵树。这棵树也表示 集合间的包含关系 -
对于
以外的状态 ,可用后缀链接 表达 : -
如果我们从任意状态
开始顺着后缀链接遍历,总会到达初始状态 。这种情况下我们可以得到一个互不相交的区间 的序列,且它们的并集形成了连续的区间
算法
-
令
为添加字符 之前,整个字符串对应的状态(一开始我们设 ,算法的最后一步更新 ) 。 -
创建一个新的状态
,并将 赋值为 ,在这时 的值还未知 -
现在我们按以下流程进行(从状态
开始)。如果还没有到字符 的转移,我们就添加一个到状态 的转移,遍历后缀链接。如果在某个点已经存在到字符 的转移,我们就停下来,并将这个状态标记为 -
如果没有找到这样的状态
,我们就到达了虚拟状态 ,我们将 赋值为 并退出 -
假设我们找到一个状态
,其可以通过字符 转移。我们将转移到状态标记为 -
现在我们分类讨论两种状态,要么
,要么不是 -
如果
,我们只要将 赋值为 并退出 -
否则我们需要 复制 状态
:我们创建一个新的状态 ,复制 的除了 值以外的所有信息(后缀链接和转移)。我们将 赋值为复制之后,我们将后缀链接从
指向 ,也从 指向最终我们需要使用后缀链接从状态
忘回走,只要存在一条通过 到状态 的转移,就将该转移重定向到状态 -
以上三种情况,在完成这个过程后,我们将
的值更新为状态
正确性证明
-
若一个转移
满足 ,我们称这个转移是 连续地 。否则,即当 时,这个转移被称为 不连续的 。从算法描述中可以看出,连续的、不连续的转移时算法的不同情况。连续的转移是固定的,我们不会再改变了,与此相反,当向字符串中插入一个新的字符时,不连续的转移可能会改变(转移边的端点可能会改变)。 -
为了避免引起歧义,我们记向 SAM 中插入当前字符
之前的字符串为 -
算法从创建一个新状态
开始,对应于整个字符串 。我们创建一个新的节点,与此同时我们也创建了一个新的字符和一个新的等价类 -
在创建一个新的状态后,我们会从对应整个字符串
的状态通过后缀链接进行遍历。对于每一个状态,我们尝试添加一个通过字符 到新状态 的转移。然而我们只能添加与原有转移不冲突的转移。因此我们只要找到已存在 的转移,我们就必须停止 -
最简单的情况是我们到达了虚拟状态 -1 ,这意味着我们为所有
的后缀添加了 的转移,这也意味着,字符 从未在字符串 中出现过。因此 的后缀链接为状态 0 -
第二种情况下,我们找到了现有的转移
,这意味着我们尝试向自动机内添加一个已经存在的字符串 (其中 为 的一个后缀,且字符串 已经作为 的一个子串出现过了 )。因为我们假设字符串 的自动机的构造是正确的,我们不应该在这里添加一个新的转移,然而,难点在于从状态 出发的后缀链接应该连接到哪个状态呢? 我们要把后缀链接接连到一个状态上,且其中最长的字符串恰好是 ,即这个状态的 是 ,然而还不存在这样的状态, ,这种情况下,我们必须通过拆开状态 来创建一个这样的状态 -
如果转移
是连续的,那么 ,这种情况下只需要将 的后缀链接指向状态 -
否则状态是不连续的,这意味着状态
不止对应于长度为 的后缀 ,还对应于 更长的子串。除了将状态 拆成两个子状态以外我们别无它法,所以第一个子状态的长度就是 了。我们如何拆开一个状态呢?我们 复制 状态
,产生一个状态 ,我们将 赋值为 ,由于我们不想改变遍历到 的路径,我们将 的所有转移复制到 ,我们也将从 出发的后缀链接设置为 的后缀链接的目标,并设置 的后缀链接为在拆开状态后,我们将从
出发的后缀链接设置为最后一步我们将一些到
的转移重定向到 。我们需要哪些修改呢?只重定向相当于所有字符串
( 是 的最长字符串)的后缀就够了。即,我们需要继续沿着后缀链接遍历,从结点 直到虚拟状态 或者转移到不是状态 的一个转移
操作次数为线性的证明(略)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】