后缀自动机 (SAM)
一些记号
:字符集。字符集大小 。 :字符串。字符串长 ,标号自 开始。 :初始状态。 :字符串 中子串 的结束位置的集合。 :状态 的后缀链接。 :状态 对应的最长子串的长度。 :状态 对应的最长子串。 :状态 对应的最短子串的长度。 :状态 对应的最短子串。
后缀自动机概述
后缀自动机(suffix automaton, SAM)是一个能解决许多字符串相关问题的有力的数据结构。
举个例子,以下的字符串问题都可以在线性时间内通过 SAM 解决:
- 在另一个字符串中搜索一个字符串的所有出现位置;
- 计算给定的字符串中有多少个不同的子串。
直观上,字符串的 SAM 可以理解为给定字符串的 所有子串 的压缩形式。值得注意的事实是,SAM 将所有的这些信息以高度压缩的形式储存。对于一个长度为
定义
字符串
换句话说:
- SAM 是一张有向无环图。结点被称作 状态,边被称作状态间的 转移。
- 图存在一个源点
,称作 初始状态,其它各结点均可从 出发到达。 - 每个 转移 都标有某个字符。从一个结点出发的所有转移均 不同。
- 存在一个或多个 终止状态。如果我们从初始状态
出发,最终转移到了一个终止状态,则路径上的所有转移的标号连接起来一定是字符串 的一个后缀。反过来, 的每个后缀均可用一条从 到某个终止状态的路径构成。 - 在所有满足上述条件的自动机中,SAM 的结点数是最少的。
SAM 的关键恰在于这个最小性。实际上,直接对字符串
子串和路径
SAM 最简单、也最重要的性质是,它包含关于字符串
为了简化表达,我们称子串 对应 这条(从
到达某个状态的路径可能不止一条,因此我们说一个状态对应一些字符串的集合,这个集合中的字符串分别对应着这些路径。
简单例子
我们将会在这里展示一些简单的字符串的后缀自动机。
我们用蓝色表示初始状态,用绿色表示终止状态。
对于字符串
对于字符串
对于字符串
对于字符串
对于字符串
对于字符串
在最后这个例子中可以看到,如果直接建立它的所有后缀的 AC 自动机,路径
线性复杂度的构造算法
在我们描述线性时间内构造 SAM 的算法之前,我们需要引入两个对理解构造过程非常重要的概念,并对其性质进行简单证明。其中,结束位置
结束位置 endpos
考虑字符串
两个子串
一个事实是,每个这样的等价类都对应 SAM 的一个状态1。也就是说,只要两个子串的结束位置相同,它们在 SAM 中的路径就对应着同一个状态。换句话说,SAM 中的每个非初始状态都对应一个或多个
暂且接受这个事实,我们将基于它介绍构造 SAM 的算法。我们还将说明,SAM 需要满足的所有性质,除了最小性以外都满足了;而最小性可以由 Myhill–Nerode 定理得出(不会在这篇文章中证明)。
由
引理 1
字符串
证明
引理显然成立。如果
引理 2
考虑两个非空子串
证明
如果集合
引理 3
考虑一个
证明
如果等价类中只包含一个子串,引理显然成立。现在我们来讨论子串元素个数大于
由引理 1,
记
一句话概括,同一个状态对应的子串的长度各不相同,而且是连续的若干自然数,其中较短的总是较长的子串的后缀。
后缀链接 link
考虑 SAM 中某个状态
我们还知道字符串
换句话说,
为方便讨论,我们规定初始状态
引理 4
所有后缀链接构成一棵根节点为
证明
考虑任意状态
引理 5
以
证明
由引理 2,任意一个 SAM 的
我们现在考虑任意状态
注意这里应该是
结合前面的引理有:后缀链接构成的树本质上是
以下是对字符串
结合图示,如果能够形成一些对于后缀自动机的认识,将对下文理解其构造算法和应用都有所帮助。
对图示的解释
- SAM 上存在一条最长的路径,其标号恰好为字符串
本身。该路径从初始状态开始,经过的每个状态都对应着字符串 的前缀( )。这些状态在后续 应用 中至关重要。 -
后缀链接树可以看做是将这些「前缀状态」沿着后缀链接移动到根节点(即初始状态)的路径「压缩」得到。
- 沿着每条路径,结点对应的字符串集合构成了相应的前缀的所有后缀的分划。例如,标记为
的状态沿着后缀链接移动到根节点的路径为 。其中,结点 实际对应着字符串集合 ,结点 实际对应着字符串集合 ,结点 就对应空字符串。 - 不同的路径可能共用同一个结点,这就是为什么会有「压缩」。例如,路径
和路径 共用了结点 。这是因为 ,而结束在位置 的字符串 前紧接着字符 而结束在位置 的字符串 前紧接着字符 ,因此,当在前方添加字符(即逆着后缀链接移动)时,结束位置集合(即状态)会分裂。 - 后缀链接树只需要将这些后缀路径合理地「压缩」在一起即可,而不需要考虑别的结点。这是因为,所有子串都是某个前缀的后缀,故而必然出现在某个这样的路径中。后文的构造算法本质上就是逐个添加字符,并为每个新增加的前缀,构造这样一条后缀路径,并使其合理地「压缩」到之前已有的路径中(即不重复构造已经存在的状态和转移)。
- 终止状态恰为字符串
本身所在的后缀路径上的所有结点。
- 沿着每条路径,结点对应的字符串集合构成了相应的前缀的所有后缀的分划。例如,标记为
-
到达同一个状态的转移必然具有相同的标号,而且这些转移的起点一定是位于后缀链接树上的某条(连续的)路径。比如,转移到状态
的状态就有两个: 和 。它们位于后缀树上的路径 上。注意,它们分别对应于字符串集合 和 ,这些字符串在后面添加字符 ,就得到状态 对应的字符串集合 。- 添加字符后,不同状态可能转移到同一个状态,是因为新添加的字符使得结束位置的增加更为困难。
-
后缀链接树上,每个结点的
集合都是其子节点的 集合的并集,至多再增加一个位置。而且,这个新位置存在,当且仅当该结点恰好对应着结束在该位置的原字符串的前缀。因为图示中,后缀链接树的非根非叶的结点都不对应着字符串 的前缀,所以不存在这种情形。
后缀自动机中存储着字符串全部子串的信息。这件事可以通过两个角度理解:
- SAM 本身可以看作是字符串全体后缀的 AC 自动机的压缩版本。因此,它存储了字符串的全体后缀的所有前缀的信息,这就相当于存储了字符串全体子串的信息。
- SAM 的后缀链接树可以看做是字符串全体前缀的后缀路径的压缩版本。因此,它存储了字符串的全体前缀的所有后缀的信息,这也相当于存储了字符串全体子串的信息。
这两种思考的角度在处理不同问题时都是有用的。
小结
在进一步讨论算法本身前,我们总结一下之前的内容,并引入一些辅助记号。
-
的子串可以根据它们的结束位置集合 划分为多个等价类; -
SAM 由初始状态
和与每一个(非空子串的) 等价类对应的每个状态组成; -
每一个状态
都匹配一个或多个子串。我们记 为其中最长的一个字符串,记 为它的长度。类似地,记 为最短的子串,它的长度为 。那么对应这个状态的所有字符串都是字符串 的不同的后缀,且所有字符串的长度恰好取遍区间 中的每一个整数。 -
对于任意状态
,定义后缀链接为连接到对应字符串 的长度为 的后缀的一条边。从根节点 出发的后缀链接可以形成一棵树。这棵树也表示 集合间的包含关系。 -
对于任意状态
,可用后缀链接 表达 : -
如果我们从任意状态
开始顺着后缀链接遍历,总会到达初始状态 。这种情况下我们可以得到一个互不相交的区间 的序列,且它们的并集形成了连续的区间 。
算法
现在我们可以讨论构造 SAM 的算法了。这个算法是 在线 算法,我们可以逐个加入字符串中的每个字符,并且在每一步中对应地维护 SAM。
在讨论详细的实现之前,首先通过图示初步感受一下增加新字符
简单理解增量构造过程
在字符串
我们首先考虑在添加新字符
图中,原字符串
-
原字符串
的后缀路径上,没有经由 的转移的结点一定是最初的几个结点。只要从某个结点(图中的 )开始,存在经由 的转移,后续经过的结点也一定存在经由 的转移。解释:设
,那么后续经过的所有结点都对应 的后缀,因而如果 也出现在 中,那么 的后缀再加 的结果也一定出现在 中,因此这些结点都有经由 的转移。 -
虽然经由字符
能够到达结点 的结点一定是后缀链接树上的连续段,但是这个连续段未必全体都位于自 到根的后缀路径上。特别地,只有第一个结点 对应的连续段中起始的若干个结点 可能 不在这个后缀路径上。例如图中的结点 就对应结点 ,其中, 不在 的后缀路径上。解释:设
,则 对应着 ,但是图中显然有 ,因为后者是 。这就说明, 对应的部分字符串不能由 及其后缀转移来。反过来, 中的字符串必然是 的后缀,因此删去末尾的 后必然是 的后缀。也就是说,经由 转移到 的结点必然在 起始的后缀路径上。这也是为什么只有起始的 对应的连续段中的部分结点可能不在 的后缀路径上。
对于这个图示,如果要在原字符串
因为结点
除了这些显然的事实外,还可以注意到,原来的结点
从 SAM 中状态代表的含义看,每个状态都是一个
以上说明的是最复杂、最一般的情形(即下文的 情形三)。实际操作时,可能并不存在结点
掌握了新增后缀路径的思想后,现在讨论增量构造的具体步骤。
过程
为了保证线性的空间复杂度,我们将只保存
一开始 SAM 只包含一个状态
现在,只需要实现给当前字符串添加一个字符
SAM 增量构造过程
- 令
为添加字符 之前,整个字符串对应的状态(一开始我们设 ,算法的最后一步更新 )。 - 创建一个新的状态
,并将 赋值为 ,在这时 的值还未知。 - 现在我们进行如下流程:从状态
开始,如果当前状态还没有标号为字符 的转移,我们就添加一个经字符 到状态 的转移,并将当前状态沿后缀链接移动。如果过程中遇到某个状态已经存在到字符 的转移,我们就停下来,并将这个状态标记为 。 - 情况一:如果没有找到这样的状态
,我们就到达了虚拟状态 ,我们将 赋值为 并退出。 - 假设现在我们找到了一个状态
,它可以通过字符 转移。我们将转移到的状态标记为 。此时,要么 ,要么 。 - 情况二:如果
,我们只要将 赋值为 并退出。 -
情况三:否则就会有些复杂,需要 复制 状态
:我们创建一个新的状态 ,复制 的除了 的值以外的所有信息(后缀链接和转移)。我们将 赋值为 。复制之后,我们将后缀链接从
指向 ,也从 指向 。最终我们需要沿着后缀链接从状态
往回走,只要经过的状态存在指向状态 的转移,就将该转移重新连接到状态 。 -
处理完以上三种情况后,我们都需要将
的值更新为状态 。
如果我们还想知道哪些状态是 终止状态 而哪些不是,我们可以在为字符串
因为我们只为
解释
我们详细解释算法每一步的细节,并说明它的 正确性。
对算法的详细解释
-
若一个转移
满足 ,则我们称这个转移是 连续的。否则,即当 时,这个转移被称为 不连续的。从算法描述中可以看出,连续的和不连续的转移,在算法中的处理也并不相同。连续的转移是固定的,我们不会再改变了。与此相反,当向字符串中插入一个新的字符时,不连续的转移可能会改变(转移边的端点可能会改变)。
-
为了避免引起歧义,我们记向 SAM 中插入当前字符
之前的字符串为 。 - 算法从创建一个新状态
开始,对应于整个字符串 。我们创建一个新的节点的原因很清楚。与此同时我们也创建了一个新的字符和一个新的等价类。 -
在创建一个新的状态之后,我们会从对应整个字符串
的状态 沿着后缀链接进行移动。对于经过的每一个状态,我们尝试添加一个通过字符 到新状态 的转移。然而我们只能添加与原有转移不冲突的转移。因此我们只要找到已存在的
的转移,我们就必须停止。 -
最简单的情况是我们到达了虚拟状态
,这意味着我们为所有 的后缀添加了 的转移。这也意味着,字符 从未在字符串 中出现过。因此 的后缀链接为状态 。 -
第二种情况下,我们找到了现有的转移
。这意味着我们尝试向自动机内添加一个 已经存在的 字符串 (其中 为 的一个后缀,且字符串 已经作为 的一个子串出现过了)。因为我们假设字符串 的自动机的构造是正确的,我们不应该在这里添加一个新的转移。然而,难点在于,从状态
出发的后缀链接应该连接到哪个状态呢?我们要把后缀链接连到一个状态上,且对应的最长的字符串恰好是 ,即这个状态的 应该是 。然而这样的状态有可能并不存在,即 。这种情况下,我们必须通过拆开状态 来创建一个这样的状态。 -
当然,如果转移
是连续的,那么 。在这种情况下一切都很简单。我们只需要将 的后缀链接指向状态 。 -
否则转移是不连续的,即
,这意味着状态 不只对应于长度为 的后缀 ,还对应于 的更长的子串。除了将状态 拆成两个子状态以外我们别无他法,所以第一个子状态的长度就是 了。我们如何拆开一个状态呢?我们 复制 状态
,产生一个状态 ,我们将 赋值为 。由于我们不想改变经过 的路径,我们将 的所有转移复制到 。我们也将从 出发的后缀链接设置为 的后缀链接的目标,并设置 的后缀链接为 。在拆开状态后,我们将从
出发的后缀链接设置为 。最后一步我们将一些原本指向
的转移重新连接到 。我们需要修改哪些转移呢?只重新连接相当于所有字符串 (其中 是状态 对应的最长字符串)的后缀就够了。也就是说,我们需要继续沿着后缀链接移动,从结点 直到虚拟状态 ,或者当前状态经 的转移不再指向状态 。
线性时间复杂度
我们假设字符集大小为 常数,即每次对一个字符搜索转移、添加转移、查找下一个转移这些操作的时间复杂度都为
证明
如果我们考虑算法的各个部分,算法中有三处时间复杂度不明显是线性的:
- 第一处是遍历所有状态
的后缀链接,添加字符 的转移。 - 第二处是当状态
被复制到一个新的状态 时复制转移的过程。 - 第三处是修改指向
的转移,将它们重新连接到 的过程。
我们使用 SAM 的大小(状态数和转移数)为 线性的 的事实(对状态数是线性的的证明就是算法本身,对转移数为线性的的证明将在稍后实现算法后给出)。
因此上述 第一处和第二处 的总复杂度显然为线性的,因为单次操作均摊只为自动机添加了一个新转移。
还需为 第三处 估计总复杂度,我们将最初指向
因为作为当前字符串后缀的字符串
当然,如果字符集大小不是常数,SAM 的时间复杂度就不是线性的。从一个结点出发的转移需要存储在支持快速查询和插入的平衡树中。因此如果我们记
实现
首先,我们实现一种存储一个转移的全部信息的数据结构。如果需要的话,你可以在这里加入一个终止标记,也可以是一些其它信息。我们将用一个 map
存储转移的列表,允许我们在总计 next
声明为 int[K]
更方便。
1 2 3 4 |
|
SAM 本身将会存储在一个 state
结构体数组中。我们记录当前自动机的大小 sz
和变量 last
,当前整个字符串对应的状态。
1 2 3 |
|
我们定义一个函数来初始化 SAM(创建一个只有初始状态的 SAM)。
1 2 3 4 5 6 |
|
最终我们给出主函数的实现:给当前行末增加一个字符,对应地在之前的基础上建造自动机。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
正如之前提到的一样,如果你用内存换时间(空间复杂度为
更多性质
状态数
对于一个长度为
证明
算法本身即可证明该结论。一开始,自动机含有一个状态,第一次和第二次迭代中只会创建一个节点,剩余的
然而我们也能在 不借助这个算法 的情况下 证明 这个估计值。我们回忆一下状态数等于不同的
字符串
转移数
对于一个长度为
证明
我们首先估计连续的转移的数量。考虑自动机中由从状态
现在我们来估计不连续的转移的数量。令当前不连续转移为
将以上两个估计值相加,我们可以得到上界
因此我们可以获得更为紧确的 SAM 的转移数的上界:
后缀链接树
尽管构造 SAM 是为了得到它的状态和转移的信息,但是构造过程中记录的后缀链接
在构建 SAM 的过程中,需要更新
引理 4 中提及,所有状态和所有后缀链接构成根为
后缀链接树有如下性质:
- 祖先节点对应的字符串总是子孙节点对应的字符串的后缀。
- 每个节点处的
集合就是它的子树内的所有「前缀节点」 的下标 的集合。 - 后缀链接树的祖先节点的
集合总是严格包含子孙节点的 集合。 - 每个节点处的
的值就是它的子树内的所有「前缀节点」 对应前缀的最长公共后缀的长度。 - 除根节点
外,每个节点对应的不同子串的数目,就是它的 值,减去它的父节点的 值,即 。
这些性质有很多应用。比如,第
最后,对字符串
应用
下面我们来看一些可以用 SAM 解决的问题。简单起见,假设字符集的大小
检查字符串是否出现
问题
给一个文本串
解法
我们在
对于每个字符串
不同子串个数
问题
给一个字符串
解法一
对字符串
每个
考虑到 SAM 为有向无环图,不同路径的条数可以通过动态规划计算。即令
即,
所以不同子串的个数为
总时间复杂度为:
解法二
另一种方法是在构造完后缀自动机后,利用得到的后缀链接树的信息。每个节点对应的子串数量是
总时间复杂度仍然为:
所有不同子串的总长度
问题
给定一个字符串
解法一
本题做法与上一题类似,只是现在我们需要考虑分两部分进行动态规划:不同子串的数量
我们已经在上一题中介绍了如何计算
我们取每个邻接结点
算法的时间复杂度仍然是
解法二
同样可以利用后缀链接树的信息。每个节点对应的最长子串的所有后缀长度是
减去其
总时间复杂度仍然为:
字典序第 k 大子串
问题
给定一个字符串
解法
解决这个问题的思路可以从解决前两个问题的思路发展而来。字典序第
预处理的时间复杂度为
另注
虽然该题是后缀自动机的经典题,但实际上这题由于涉及字典序,用后缀数组做最方便。
最小循环移位
问题
给定一个字符串
解法
容易发现字符串
所以问题简化为在
总的时间复杂度为
出现次数
问题
对于一个给定的文本串
解法一
利用后缀链接树的信息,进行 dfs 即可预处理每个节点的
所有「前缀节点」的初始集合大小为
查询时,在自动机上查找模式串
预处理时间复杂度为
解法二
对文本串
接下来做预处理:对于自动机中的每个状态
然而我们不能明确的构造集合
为了计算这些值,我们进行以下操作。对于每个状态,如果它不是通过复制创建的(且它不是初始状态
这样做每个状态的答案都是正确的。
为什么这是正确的?不是通过复制获得的状态,恰好有
接下来我们对每一个
为什么我们在这个过程中不会重复计数(即把某些位置数了两次)呢?因为我们只将一个状态的位置添加到 一个 其它的状态上,所以一个状态不可能以两种不同的方式将其位置重复地指向另一个状态。
因此,我们可以在
最后回答询问只需要查找值
第一次出现的位置
问题
给定一个文本串
解法一
利用后缀链接树的信息,进行 dfs 即可预处理每个节点的
所有「前缀节点」
查询时,在自动机上查找模式串
预处理时间复杂度为
解法二
我们构造一个后缀自动机。我们对 SAM 中的所有状态预处理位置
为了维护 sam_extend()
进行扩展。当我们创建新状态
当我们将结点
(因为值的唯一的其它选项
那么查询的答案就是
所有出现的位置
问题
问题同上,这一次需要查询文本串
解法一
找到模式串
单次查询复杂度为
解法二
我们还是对文本串
如果
因此为了解决这个问题,我们需要为每一个状态保存一个指向它的后缀连接列表。查询的答案就包含了对于每个我们能从状态
预处理的复杂度为
我们不会重复访问一个状态(因为对于仅有一个后缀链接指向一个状态,所以不存在两条不同的路径指向同一状态)。
我们只需要考虑两个可能有相同
此外,我们可以通过不考虑复制而来的节点的 is_clone
来代表这个状态是不是被复制出来的,我们就可以简单地忽略掉被复制的状态,只输出其它所有状态的
以下是大致的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
最短的没有出现的字符串
问题
给定一个字符串
解法
我们在字符串
假定我们已经处理完了子串的一部分,当前在状态
计算
问题的答案就是
两个字符串的最长公共子串
问题
给定两个字符串
解法
我们对字符串
我们现在处理字符串
为了达到这一目的,我们使用两个变量,当前状态
一开始
现在我们来描述如何添加一个字符
- 如果存在一个从
到字符 的转移,我们只需要转移并让 自增一。 -
如果不存在这样的转移,我们需要缩短当前匹配的部分,这意味着我们需要按照后缀链接进行转移:
与此同时,需要缩短当前长度。显然我们需要将
赋值为 ,因为经过这个后缀链接后我们到达的状态所对应的最长字符串是一个子串。 -
如果仍然没有使用这一字符的转移,我们继续重复经过后缀链接并减小
,直到我们找到一个转移或到达虚拟状态 (这意味着字符 根本没有在 中出现过,所以我们设置 )。
显然问题的答案就是所有
这一部分的时间复杂度为
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
例题:SPOJ Longest Common Substring
多个字符串间的最长公共子串
问题
给定
解法一
我们将所有的子串连接成一个较长的字符串
然后对字符串
现在我们需要在自动机中找到存在于所有字符串
因此我们需要计算可达性,即对于自动机中的每个状态和每个字符
解法二
不妨设 最短 的字符串为
因为匹配过程中,每次匹配到 SAM 的一个状态时,必然同时匹配到了它在后缀链接树上的所有祖先节点,但是祖先节点的匹配长度的信息并没有更新。所以,在完成对字符串
最后,只需要对每个
算法时间复杂度是
例题:SPOJ Longest Common Substring II
习题
- HihoCoder #1441 : 后缀自动机一·基本概念
- 【模板】后缀自动机
- SDOI2016 生成魔咒
- SPOJ - SUBLEX
- TJOI2015 弦论
- SPOJ Longest Common Substring
- SPOJ Longest Common Substring II
- Codeforces 1037H Security
- Codeforces 666E Forensic Examination
- HDU4416 Good Article Good sentence
- HDU4436 str2int
- HDU6583 Typewriter
- Codeforces 235C Cyclical Quest
- CTSC2012 熟悉的文章
- NOI2018 你的名字
相关资料
我们先给出与 SAM 有关的最初的一些文献:
- A. Blumer, J. Blumer, A. Ehrenfeucht, D. Haussler, R. McConnell. Linear Size Finite Automata for the Set of All Subwords of a Word. An Outline of Results. [1983]
- A. Blumer, J. Blumer, A. Ehrenfeucht, D. Haussler. The Smallest Automaton Recognizing the Subwords of a Text. [1984]
- Maxime Crochemore. Optimal Factor Transducers. [1985]
- Maxime Crochemore. Transducers and Repetitions. [1986]
- A. Nerode. Linear automaton transformations. [1958]
另外,在更新的一些资源以及很多关于字符串算法的书中,都能找到这个主题:
- Maxime Crochemore, Rytter Wowjcieh. Jewels of Stringology. [2002]
- Bill Smyth. Computing Patterns in Strings. [2003]
- Bill Smith. Methods and algorithms of calculations on lines. [2006]
另外,还有一些资料:
- 《后缀自动机》,陈立杰。
- 《后缀自动机在字典树上的拓展》,刘研绎。
- 《后缀自动机及其应用》,张天扬。
- https://www.cnblogs.com/zinthos/p/3899679.html
- https://codeforces.com/blog/entry/20861
- https://zhuanlan.zhihu.com/p/25948077
本页面主要译自博文 Суффиксный автомат 与其英文翻译版 Suffix Automaton。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。
-
需要将每个状态都取作一个
等价类的原因,其实就是本段提到的 Myhill–Nerode 定理。简单来说,如果两个字符串 和 的 集合不同,那么它们不能对应于 SAM 的同一个状态:同一个状态到达终止状态的路径总是一样的,这意味着在 和 末尾添加字符到达 的结尾的方式也是一样的,而这正说明 和 在字符串 中的结束位置一样。反过来,只要两个字符串 和 的 集合相同,就可以将它们对应到 SAM 的同一个状态。这样做可行,就是 Nerode 定理的证明的内容,在此不多讨论。但是,此处的讨论至少可以相信,将 集合相同的字符串放到同一个状态,这样得到的 SAM 一定是最小的,因为进一步合并节点是不可能的。 ↩ -
如果不额外使用列表记录当前状态的可用转移,只用数组存储所有可能的转移(无论是否存在)并在复制节点时直接复制,那么时间复杂度也是
的。 ↩↩ -
此处正文没有解释的是,在第一种和第二种情况中,
的位置是否也是单调(弱)递增的。第一种情况容易验证,因为更新后 是空串,起止位置在字符串 的末尾。第二种情况,转移是连续的,说明 。然而,向子串的末尾添加新的字符只会使得该子串更难以出现在字符串中,也就是说,当字符串 的长度为 的后缀的结束位置集合严格包含 时,字符串 的长度为 的后缀的结束位置集合可能仍然与 相同。故而, ,亦即 作为 的后缀的起始位置必然不大于 作为 的后缀的起始位置。而当一次找到状态 使得存在经由 的转移时,必定移动了至少一次,这说明 作为 的后缀的起始位置不小于 作为 的后缀的起始位置。最后, 。这就说明,在第二种情况中, 的位置也是单调递增的。 ↩
本页面最近更新:2025/5/11 20:20:29,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:Chrogeek, GoodCoder666, ksyx, abc1763613206, Backl1ght, c-forrest, CCXXXI, ChungZH, countercurrent-time, Dev-XYS, Enter-tainer, FFjet, frank-xjh, fstqwq, GeZiyue, Ghastlcon, H-J-Granger, Heartfirey, Henry-ZHR, iamtwz, interestingLSY, Ir1d, Kinandra, LeoJacob, longlongzhu123, Luckyblock233, mao1t, Marcythm, mgt, MingqiHuang, opsiff, ouuan, ranwen, shuzhouliu, sshwy, SukkaW, Suyun514, Tiphereth-A, vincent-163, WhenMelancholy, Xeonacid, yjl9903, ZXyaang
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用