一些记号

  • :字符集。字符集大小
  • :字符串。字符串长 ,标号自 开始。
  • :初始状态。
  • :字符串 中子串 的结束位置的集合。
  • :状态 的后缀链接。
  • :状态 对应的最长子串的长度。
  • :状态 对应的最长子串。
  • :状态 对应的最短子串的长度。
  • :状态 对应的最短子串。

后缀自动机概述

后缀自动机(suffix automaton, SAM)是一个能解决许多字符串相关问题的有力的数据结构。

举个例子,以下的字符串问题都可以在线性时间内通过 SAM 解决:

  • 在另一个字符串中搜索一个字符串的所有出现位置;
  • 计算给定的字符串中有多少个不同的子串。

直观上,字符串的 SAM 可以理解为给定字符串的 所有子串 的压缩形式。值得注意的事实是,SAM 将所有的这些信息以高度压缩的形式储存。对于一个长度为 的字符串,它的空间复杂度仅为 。而且,构造 SAM 的时间复杂度也仅为 。准确地说,一个 SAM 最多有 个结点和 条转移边。

定义

字符串 的 SAM 是一个接受 的所有后缀的最小 DFA

换句话说:

  • SAM 是一张有向无环图。结点被称作 状态,边被称作状态间的 转移
  • 图存在一个源点 ,称作 初始状态,其它各结点均可从 出发到达。
  • 每个 转移 都标有某个字符。从一个结点出发的所有转移均 不同
  • 存在一个或多个 终止状态。如果我们从初始状态 出发,最终转移到了一个终止状态,则路径上的所有转移的标号连接起来一定是字符串 的一个后缀。反过来, 的每个后缀均可用一条从 到某个终止状态的路径构成。
  • 在所有满足上述条件的自动机中,SAM 的结点数是最少的。

SAM 的关键恰在于这个最小性。实际上,直接对字符串 的所有后缀建立 AC 自动机 同样可以得到一个接受 的所有后缀的 DFA。但是,最差情况下,这样得到的自动机有 个结点,复杂度难以接受。从下面的例子可以看出,对所有后缀建立 AC 自动机得到的 DFA 中很多结点是重复的,因而可以合并。SAM 正是将结点的合并做到了极致,故而将得到的 DFA 的规模控制在 。从这个意义上,SAM 是字符串的全体后缀的「压缩」的 AC 自动机。

子串和路径

SAM 最简单、也最重要的性质是,它包含关于字符串 的所有子串的信息。任意从初始状态 开始的路径,如果我们将路径上的所有转移的标号写下来,都会形成 的一个 子串。反之,每个 的子串都对应从 开始的某条路径。

为了简化表达,我们称子串 对应 这条(从 出发且它上面所有转移的标号构成这个子串的)路径。反过来,我们说任意一条路径都 对应 它的标号构成的字符串。

到达某个状态的路径可能不止一条,因此我们说一个状态对应一些字符串的集合,这个集合中的字符串分别对应着这些路径。

简单例子

我们将会在这里展示一些简单的字符串的后缀自动机。

我们用蓝色表示初始状态,用绿色表示终止状态。

对于字符串

对于字符串

对于字符串

对于字符串

对于字符串

对于字符串

在最后这个例子中可以看到,如果直接建立它的所有后缀的 AC 自动机,路径 和路径 应当导向不同的结点,但是这两个结点都是终止状态,且无论再添加任何字符,都不会得到更长的匹配串,这说明两个结点在自动机的转移上表现出同样的性质,因而可以合并成同一个结点。这样就得到了如图所示的 SAM。下面的讨论会将合并结点这一想法拓展到所有的情形,并说明,只要合理地合并结点,最后得到的 SAM 只有 个结点和转移。

线性复杂度的构造算法

在我们描述线性时间内构造 SAM 的算法之前,我们需要引入两个对理解构造过程非常重要的概念,并对其性质进行简单证明。其中,结束位置 定义了 SAM 中的结点(亦即指出了结点可以合并的充要条件),而后缀链接 不过是 AC 自动机中的 失配指针 在 SAM 中的自然对应。

结束位置 endpos

考虑字符串 的任意非空子串 ,我们记 为在字符串 的所有结束位置的集合(假设对字符串中字符的编号从零开始)。例如,对于字符串 ,我们有

两个子串 的结束位置可能完全相同:。这定义了字符串 的子串之间的等价关系。字符串 的所有非空子串可以根据它们的结束位置集合 分为若干 等价类

一个事实是,每个这样的等价类都对应 SAM 的一个状态 1。也就是说,只要两个子串的结束位置相同,它们在 SAM 中的路径就对应着同一个状态。换句话说,SAM 中的每个非初始状态都对应一个或多个 相同的非空子串。总之,SAM 中的状态就是所有非空子串的等价类,再加上初始状态。

暂且接受这个事实,我们将基于它介绍构造 SAM 的算法。我们还将说明,SAM 需要满足的所有性质,除了最小性以外都满足了;而最小性可以由 Myhill–Nerode 定理 得出。

的值我们可以得到一些重要结论,它们解释了同一个状态对应的不同的子串之间的关系。

引理 1

字符串 的两个非空子串 (假设 )的 相同,当且仅当字符串 中每次出现时,都是以 后缀的形式存在的。

引理 2

考虑两个非空子串 (假设 )。那么,要么 ,要么 ,取决于 是否为 的一个后缀:

引理 3

考虑一个 相同的子串等价类,将类中的所有子串按长度非递增的顺序排序。那么,每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任意两子串,较短者为较长者的后缀,且该等价类中的子串长度是连续的,取遍某个区间内的所有整数值。

一句话概括,同一个状态对应的子串的长度各不相同,而且是连续的若干自然数,其中较短的总是较长的子串的后缀。

考虑 SAM 中某个状态 。我们已经知道,状态 对应于具有相同 的子串等价类。我们如果定义 为这些字符串中最长的一个,则所有其它的字符串都是 的后缀。

我们还知道字符串 的前几个后缀(按长度降序考虑)全部包含于这个等价类,且其它后缀(至少有一个——空后缀)在别的等价类中。我们记 为其它后缀中最长的,然后将 的后缀链接连到 上。

换句话说,后缀链接 连接到的状态,对应于 的后缀中与它的 集合不同且最长的那个,也是 的后缀中在 中的出现次数比 更多且最长的那个。

为方便讨论,我们规定初始状态 对应的等价类,只包含一个空字符串,而且

引理 4

所有后缀链接构成一棵根节点为 的树。

引理 5

集合为结点、集合的包含关系作为边,这样构造的树(即每个子节点的 集合都包含在父节点的 集合中)与通过后缀链接 构造的树相同。

结合前面的引理有:后缀链接构成的树本质上是 集合构成的一棵树。

以下是对字符串 构造 SAM 时产生的后缀链接树的一个 例子,结点被标记为对应等价类中最长的子串。

结合图示,如果能够形成一些对于后缀自动机的认识,将对下文理解其构造算法和应用都有所帮助。

对图示的解释

  • SAM 上存在一条最长的路径,其标号恰好为字符串 本身。该路径从初始状态开始,经过的每个状态都对应着字符串 的前缀()。这些状态在后续 应用 中至关重要。

  • 后缀链接树可以看做是将这些「前缀状态」沿着后缀链接移动到根节点(即初始状态)的路径「压缩」得到。

    • 沿着每条路径,结点对应的字符串集合构成了相应的前缀的所有后缀的分划。例如,标记为 的状态沿着后缀链接移动到根节点的路径为 。其中,结点 实际对应着字符串集合 ,结点 实际对应着字符串集合 ,结点 就对应空字符串。
    • 不同的路径可能共用同一个结点,这就是为什么会有「压缩」。例如,路径 和路径 共用了结点 。这是因为 ,而结束在位置 的字符串 前紧接着字符 而结束在位置 的字符串 前紧接着字符 ,因此,当在前方添加字符(即逆着后缀链接移动)时,结束位置集合(即状态)会分裂。
    • 后缀链接树只需要将这些后缀路径合理地「压缩」在一起即可,而不需要考虑别的结点。这是因为,所有子串都是某个前缀的后缀,故而必然出现在某个这样的路径中。后文的构造算法本质上就是逐个添加字符,并为每个新增加的前缀,构造这样一条后缀路径,并使其合理地「压缩」到之前已有的路径中(即不重复构造已经存在的状态和转移)。
    • 终止状态恰为字符串 本身所在的后缀路径上的所有结点。
  • 到达同一个状态的转移必然具有相同的标号,而且这些转移的起点一定是位于后缀链接树上的某条(连续的)路径。比如,转移到状态 的状态就有两个:。它们位于后缀树上的路径 上。注意,它们分别对应于字符串集合 ,这些字符串在后面添加字符 ,就得到状态 对应的字符串集合

    • 添加字符后,不同状态可能转移到同一个状态,是因为新添加的字符使得结束位置的增加更为困难。
  • 后缀链接树上,每个结点的 集合都是其子节点的 集合的并集,至多再增加一个位置。而且,这个新位置存在,当且仅当该结点恰好对应着结束在该位置的原字符串的前缀。因为图示中,后缀链接树的非根非叶的结点都不对应着字符串 的前缀,所以不存在这种情形。

后缀自动机中存储着字符串全部子串的信息。这件事可以通过两个角度理解:

  • SAM 本身可以看作是字符串全体后缀的 AC 自动机的压缩版本。因此,它存储了字符串的全体后缀的所有前缀的信息,这就相当于存储了字符串全体子串的信息。
  • SAM 的后缀链接树可以看做是字符串全体前缀的后缀路径的压缩版本。因此,它存储了字符串的全体前缀的所有后缀的信息,这也相当于存储了字符串全体子串的信息。

这两种思考的角度在处理不同问题时都是有用的。

小结

在进一步讨论算法本身前,我们总结一下之前的内容,并引入一些辅助记号。

  • 的子串可以根据它们的结束位置集合 划分为多个等价类;

  • SAM 由初始状态 和与每一个(非空子串的) 等价类对应的每个状态组成;

  • 每一个状态 都匹配一个或多个子串。我们记 为其中最长的一个字符串,记 为它的长度。类似地,记 为最短的子串,它的长度为 。那么对应这个状态的所有字符串都是字符串 的不同的后缀,且所有字符串的长度恰好取遍区间 中的每一个整数。

  • 对于任意状态 ,定义后缀链接为连接到对应字符串 的长度为 的后缀的一条边。从根节点 出发的后缀链接可以形成一棵树。这棵树也表示 集合间的包含关系。

  • 对于任意状态 ,可用后缀链接 表达

  • 如果我们从任意状态 开始顺着后缀链接遍历,总会到达初始状态 。这种情况下我们可以得到一个互不相交的区间 的序列,且它们的并集形成了连续的区间

算法

现在我们可以讨论构造 SAM 的算法了。这个算法是 在线 算法,我们可以逐个加入字符串中的每个字符,并且在每一步中对应地维护 SAM。

在讨论详细的实现之前,首先通过图示初步感受一下增加新字符 时,SAM 可能发生的变化。

简单理解增量构造过程

在字符串 的 SAM 的基础上,可以构造字符串 的 SAM。根据前文对图示的解释,只需要构造出新增加的前缀(即 )的后缀路径,并压缩到现有的路径上即可。而且,根据前文的描述,新的后缀路径上的结点必然都可以通过原字符串 的后缀路径上的结点经由字符 转移而来。

我们首先考虑在添加新字符 之前,原来的字符串 的后缀路径可能具有什么形式,而且会怎样经由字符 转移。最一般的情形,如下图示:

图中,原字符串 的后缀路径为 ,后缀链接由红色箭头表示。其中部分结点(即 )已经存在经由字符 的转移;因为将连续的后缀串添加同一个字符会同样得到连续的后缀,所以这些转移的终点组成另一串后缀路径 。此时,有两点观察:

  • 原字符串 的后缀路径上,没有经由 的转移的结点一定是最初的几个结点。只要从某个结点(图中的 )开始,存在经由 的转移,后续经过的结点也一定存在经由 的转移。

    解释:设 ,那么后续经过的所有结点都对应 的后缀,因而如果 也出现在 中,那么 的后缀再加 的结果也一定出现在 中,因此这些结点都有经由 的转移。

  • 虽然经由字符 能够到达结点 的结点一定是后缀链接树上的连续段,但是这个连续段未必全体都位于自 到根的后缀路径上。特别地,只有第一个结点 对应的连续段中起始的若干个结点 可能 不在这个后缀路径上。例如图中的结点 就对应结点 ,其中, 不在 的后缀路径上。

    解释:设 ,则 对应着 ,但是图中显然有 ,因为后者是 。这就说明, 对应的部分字符串不能由 及其后缀转移来。反过来, 中的字符串必然是 的后缀,因此删去末尾的 后必然是 的后缀。也就是说,经由 转移到 的结点必然在 起始的后缀路径上。这也是为什么只有起始的 对应的连续段中的部分结点可能不在 的后缀路径上。

对于这个图示,如果要在原字符串 的末尾添加一个字符 ,并构造出相应的后缀路径,会发生什么变化呢?答案是如下图所示:

因为结点 由原字符串 对应结点 经由字符 转移而来,它就对应新字符串 。所以,它的后缀路径 就是新增的后缀路径。如果原来的后缀路径上的结点 本就有经由 的转移,那么新的后缀路径也必然会经过这些转移到达的结点,因此可以直接复用旧有的结点。新的后缀路径上有且只有一个完全新建的节点 ,用于接受原来的后缀路径上起始的那些没有经由 的转移的结点的转移。

除了这些显然的事实外,还可以注意到,原来的结点 也经过了一次复制,或者说是分裂成了两个结点 。这是因为新增的后缀路径只是与现有路径部分重合:原来的结点 对应的字符串中,只有较短的那些(即结点 能够转移到的那些)才会出现在新增的后缀路径上,而较长的那些(即结点 能够转移到的那些)并不会出现在新增的后缀路径上,因此新增的后缀路径只能经过结点 的一部分,后者只能分裂成两个结点用于表示这种情形。同样的道理,前面已经解释过, 之后的结点 都无法由不在 的后缀路径上的结点转移,因此这些结点对应的所有字符串都会出现在新增的后缀路径上,也就不需要分裂了。

从 SAM 中状态代表的含义看,每个状态都是一个 集合。设延长字符串时,新增的结束位置为 ,那么新增的结点 就是结束位置集合 ,而分裂的结点 分别对应集合 ,之后的结点 其实都在原有的结束位置集合上新增了 。也就是说,虽然 及其相关的转移没有发生变化,但是它们对应的 集合的确扩大了。

以上说明的是最复杂、最一般的情形(即下文的 情形三)。实际操作时,可能并不存在结点 ,因而也就不需要分裂(即下文的 情形二)。要判断这种情形,只需要判断 即可,亦即 。也有可能 的后缀路径上的所有结点都没有经由 的转移,此时,只要新建 就好了(即下文的 情形一)。

掌握了新增后缀路径的思想后,现在讨论增量构造的具体步骤。

过程

为了保证线性的空间复杂度,我们将只保存 的值和每个状态的转移列表,我们不会标记终止状态(但是我们稍后会展示在构造 SAM 后如何分配这些标记)。

一开始 SAM 只包含一个状态 ,编号为 (其它状态的编号为 )。为了方便,对于状态 我们指定 表示虚拟状态)。

现在,只需要实现给当前字符串添加一个字符 的过程。算法流程如下:

SAM 增量构造过程

  • 为添加字符 之前,整个字符串对应的状态(一开始我们设 ,算法的最后一步更新 )。

  • 创建一个新的状态 ,并将 赋值为 ,在这时 的值还未知。

  • 现在我们进行如下流程:从状态 开始,如果当前状态还没有标号为字符 的转移,我们就添加一个经字符 到状态 的转移,并将当前状态沿后缀链接移动。如果过程中遇到某个状态已经存在到字符 的转移,我们就停下来,并将这个状态标记为

  • 情况一:如果没有找到这样的状态 ,我们就到达了虚拟状态 ,我们将 赋值为 并退出。

  • 假设现在我们找到了一个状态 ,它可以通过字符 转移。我们将转移到的状态标记为 。此时,要么 ,要么

  • 情况二:如果 ,我们只要将 赋值为 并退出。

  • 情况三:否则就会有些复杂,需要 复制 状态 :我们创建一个新的状态 ,复制 的除了 的值以外的所有信息(后缀链接和转移)。我们将 赋值为

    复制之后,我们将后缀链接从 指向 ,也从 指向

    最终我们需要沿着后缀链接从状态 往回走,只要经过的状态存在指向状态 的转移,就将该转移重新连接到状态

  • 处理完以上三种情况后,我们都需要将 的值更新为状态

如果我们还想知道哪些状态是 终止状态 而哪些不是,我们可以在为字符串 构造完完整的 SAM 后找到所有的终止状态。为此,我们从对应整个字符串的状态(存储在变量 中),遍历它的后缀链接,直到到达初始状态。我们将所有遍历到的状态都标记为终止状态。容易理解这样做我们会准确地标记字符串 的所有后缀,这些状态都是终止状态。

因为我们只为 的每个字符创建一个或两个新状态,所以 SAM 只包含 线性个 状态。而 SAM 只有线性规模的转移个数,以及算法总体的线性运行时间,都还没有说清楚,将在后文说明。

解释

我们详细解释算法每一步的细节,并说明它的 正确性

对算法的详细解释

  • 若一个转移 满足 ,则我们称这个转移是 连续的。否则,即当 时,这个转移被称为 不连续的

    从算法描述中可以看出,连续的和不连续的转移,在算法中的处理也并不相同。连续的转移是固定的,我们不会再改变了。与此相反,当向字符串中插入一个新的字符时,不连续的转移可能会改变(转移边的端点可能会改变)。

  • 为了避免引起歧义,我们记向 SAM 中插入当前字符 之前的字符串为

  • 算法从创建一个新状态 开始,对应于整个字符串 。我们创建一个新的节点的原因很清楚。与此同时我们也创建了一个新的字符和一个新的等价类。

  • 在创建一个新的状态之后,我们会从对应整个字符串 的状态 沿着后缀链接进行移动。对于经过的每一个状态,我们尝试添加一个通过字符 到新状态 的转移。

    然而我们只能添加与原有转移不冲突的转移。因此我们只要找到已存在的 的转移,我们就必须停止。

  • 最简单的情况是我们到达了虚拟状态 ,这意味着我们为所有 的后缀添加了 的转移。这也意味着,字符 从未在字符串 中出现过。因此 的后缀链接为状态

  • 第二种情况下,我们找到了现有的转移 。这意味着我们尝试向自动机内添加一个 已经存在的 字符串 (其中 的一个后缀,且字符串 已经作为 的一个子串出现过了)。因为我们假设字符串 的自动机的构造是正确的,我们不应该在这里添加一个新的转移。

    然而,难点在于,从状态 出发的后缀链接应该连接到哪个状态呢?我们要把后缀链接连到一个状态上,且对应的最长的字符串恰好是 ,即这个状态的 应该是 。然而这样的状态有可能并不存在,即 。这种情况下,我们必须通过拆开状态 来创建一个这样的状态。

  • 当然,如果转移 是连续的,那么 。在这种情况下一切都很简单。我们只需要将 的后缀链接指向状态

  • 否则转移是不连续的,即 ,这意味着状态 不只对应于长度为 的后缀 ,还对应于 的更长的子串。除了将状态 拆成两个子状态以外我们别无他法,所以第一个子状态的长度就是 了。

    我们如何拆开一个状态呢?我们 复制 状态 ,产生一个状态 ,我们将 赋值为 。由于我们不想改变经过 的路径,我们将 的所有转移复制到 。我们也将从 出发的后缀链接设置为 的后缀链接的目标,并设置 的后缀链接为

    在拆开状态后,我们将从 出发的后缀链接设置为

    最后一步我们将一些原本指向 的转移重新连接到 。我们需要修改哪些转移呢?只重新连接相当于所有字符串 (其中 是状态 对应的最长字符串)的后缀就够了。也就是说,我们需要继续沿着后缀链接移动,从结点 直到虚拟状态 ,或者当前状态经 的转移不再指向状态

线性时间复杂度

我们假设字符集大小为 常数,即每次对一个字符搜索转移、添加转移、查找下一个转移这些操作的时间复杂度都为 的。如果将每个结点的转移分别存储为一个长度为 的数组(用于快速查询给定标号的转移)和一个动态列表(用于快速遍历所有可用转移),以空间换时间,那么算法的时间复杂度 2,空间复杂度为

当然,如果字符集大小不是常数,SAM 的时间复杂度就不是线性的。从一个结点出发的转移需要存储在支持快速查询和插入的平衡树中。因此如果我们记 为字符集, 为字符集大小,则算法的渐近时间复杂度为 ,空间复杂度为

实现

首先,我们实现一种存储一个转移的全部信息的数据结构。如果需要的话,你可以在这里加入一个终止标记,也可以是一些其它信息。我们将用一个 map 存储转移的列表,允许我们在总计 的空间复杂度和 的时间复杂度内处理整个字符串。当然,在字符集大小为较小的常数 (比如 26)时,将 next 声明为 int[K] 更方便。

struct state {
  int len, link;
  std::map<char, int> next;
};

SAM 本身将会存储在一个 state 结构体数组中。我们记录当前自动机的大小 sz 和变量 last,当前整个字符串对应的状态。

constexpr int MAXLEN = 100000;
state st[MAXLEN * 2];
int sz, last;

我们定义一个函数来初始化 SAM(创建一个只有初始状态的 SAM)。

void sam_init() {
  st[0].len = 0;
  st[0].link = -1;
  sz++;
  last = 0;
}

最终我们给出主函数的实现:给当前行末增加一个字符,对应地在之前的基础上建造自动机。

实现

void sam_extend(char c) {
  int cur = sz++;
  st[cur].len = st[last].len + 1;
  int p = last;
  while (p != -1 && !st[p].next.count(c)) {
    st[p].next[c] = cur;
    p = st[p].link;
  }
  if (p == -1) {
    st[cur].link = 0;
  } else {
    int q = st[p].next[c];
    if (st[p].len + 1 == st[q].len) {
      st[cur].link = q;
    } else {
      int clone = sz++;
      st[clone].len = st[p].len + 1;
      st[clone].next = st[q].next;
      st[clone].link = st[q].link;
      while (p != -1 && st[p].next[c] == q) {
        st[p].next[c] = clone;
        p = st[p].link;
      }
      st[q].link = st[cur].link = clone;
    }
  }
  last = cur;
}

正如之前提到的一样,如果你用内存换时间(空间复杂度为 ,其中 为字符集大小),你可以在 的时间 2 内构造字符集大小任意的 SAM。但是这样你需要为每一个状态储存一个大小为 的数组(用于快速根据字符找到相应的转移)以及一个包含所有可用转移的列表(用于快速遍历所有可用的转移)。

更多性质

状态数

对于一个长度为 的字符串 ,它的 SAM 中的状态数 不会超过 (假设 )。

转移数

对于一个长度为 的字符串 ,它的 SAM 中的转移数 不会超过 (假设 )。

后缀链接树

尽管构造 SAM 是为了得到它的状态和转移的信息,但是构造过程中记录的后缀链接 和该状态对应的最长子串长度 在应用中常常比 SAM 的转移更为重要,甚至可以抛开转移单独使用。

在构建 SAM 的过程中,需要更新 状态的值。它对应的是每次添加字符前(后)的字符串,也就是整个字符串 的所有前缀。将第 个前缀对应的状态记为 ,这样就得到 共计 个状态。另外,规定初始状态 ,对应着空前缀。这些状态姑且称为「前缀节点」。

引理 4 中提及,所有状态和所有后缀链接构成根为 的根向树,这个树也称为 后缀链接树(国内 OI 选手也常称它为 parent 树)。它记录了字符串全体前缀的所有后缀的信息,亦即全体子串的信息。

后缀链接树有如下性质:

  • 祖先节点对应的字符串总是子孙节点对应的字符串的后缀。
  • 每个节点处的 集合就是它的子树内的所有「前缀节点」 的下标 的集合。
  • 后缀链接树的祖先节点的 集合总是严格包含子孙节点的 集合。
  • 每个节点处的 的值就是它的子树内的所有「前缀节点」 对应前缀的最长公共后缀的长度。
  • 除根节点 外,每个节点对应的不同子串的数目,就是它的 值,减去它的父节点的 值,即

这些性质有很多应用。比如,第 个前缀和第 个前缀的最长公共后缀对应的字符串就是 的 LCA 对应的最长字符串。

最后,对字符串 建立的后缀链接树与对它的翻转 建立的 后缀树 有相同的结构。这一点常常用于离线构造后缀树。

应用

下面我们来看一些可以用 SAM 解决的问题。简单起见,假设字符集的大小 为常数。这允许我们认为增加一个字符和遍历的复杂度为常数。

检查字符串是否出现

问题

给一个文本串 和多个模式串 ,我们要检查字符串 是否作为 的一个子串出现。

不同子串个数

问题

给一个字符串 ,计算不同子串的个数。

例题:【模板】后缀自动机SDOI2016 生成魔咒

所有不同子串的总长度

问题

给定一个字符串 ,计算所有不同子串的总长度。

字典序第 k 大子串

问题

给定一个字符串 。多组询问,每组询问给定一个数 ,查询 的所有子串中字典序第 大的子串。

例题:SPOJ - SUBLEXTJOI2015 弦论

最小循环移位

问题

给定一个字符串 。找出字典序最小的循环移位。

出现次数

问题

对于一个给定的文本串 ,有多组询问,每组询问给一个模式串 ,回答模式串 在字符串 中作为子串出现了多少次。

第一次出现的位置

问题

给定一个文本串 ,多组查询。每次查询字符串 在字符串 中第一次出现的位置( 的开头位置)。

所有出现的位置

问题

问题同上,这一次需要查询文本串 中模式串 出现的所有位置。

最短的没有出现的字符串

问题

给定一个字符串 和一个特定的字符集,我们要找一个长度最短的没有在 中出现过的字符串。

两个字符串的最长公共子串

问题

给定两个字符串 ,求出最长公共子串,公共子串定义为在 中都作为子串出现过的字符串

例题:SPOJ Longest Common Substring

多个字符串间的最长公共子串

问题

给定 个字符串 。我们需要找到它们的最长公共子串,即作为子串出现在每个字符串中的字符串

例题:SPOJ Longest Common Substring II

习题

相关资料

我们先给出与 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]

另外,还有一些资料:

本页面主要译自博文 Суффиксный автомат 与其英文翻译版 Suffix Automaton。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。

Footnotes

  1. 需要将每个状态都取作一个 等价类的原因,其实就是本段提到的 Myhill–Nerode 定理。简单来说,如果两个字符串 集合不同,那么它们不能对应于 SAM 的同一个状态:同一个状态到达终止状态的路径总是一样的,这意味着在 末尾添加字符到达 的结尾的方式也是一样的,而这正说明 在字符串 中的结束位置一样。反过来,只要两个字符串 集合相同,就可以将它们对应到 SAM 的同一个状态。这样做可行,就是 Nerode 定理的证明的内容,在此不多讨论。但是,此处的讨论至少可以相信,将 集合相同的字符串放到同一个状态,这样得到的 SAM 一定是最小的,因为进一步合并节点是不可能的。

  2. 如果不额外使用列表记录当前状态的可用转移,只用数组存储所有可能的转移(无论是否存在)并在复制节点时直接复制,那么时间复杂度也是 的。 2

  3. 此处正文没有解释的是,在第一种和第二种情况中, 的位置是否也是单调(弱)递增的。第一种情况容易验证,因为更新后 是空串,起止位置在字符串 的末尾。第二种情况,转移是连续的,说明 。然而,向子串的末尾添加新的字符只会使得该子串更难以出现在字符串中,也就是说,当字符串 的长度为 的后缀的结束位置集合严格包含 时,字符串 的长度为 的后缀的结束位置集合可能仍然与 相同。故而,,亦即 作为 的后缀的起始位置必然不大于 作为 的后缀的起始位置。而当一次找到状态 使得存在经由 的转移时,必定移动了至少一次,这说明 作为 的后缀的起始位置不小于 作为 的后缀的起始位置。最后,。这就说明,在第二种情况中, 的位置也是单调递增的。

此文件夹下有0条笔记。