1. 前言

后缀自动机是一种高效的有穷状态机,用于表示一个字符串的所有可能的后缀子串。与传统的后缀树相比,后缀自动机具有较小的空间复杂度和较快的构建速度。

它就是一个要实现能存下一个串中所有子串的算法,按一般来说应当有个状态,而 SAM 却可以用 O(N) 个状态来表示所有子串,因为它把很多个本质相似的子串映射到了同一个状态上。


2. 基础概念

a. 状态
  • 定义:状态表示了所有具有相同最长公共前缀的子串。
  • 属性
    • len:该状态所代表的子串的最大长度。
    • link:指向另一个状态的指针,用于表示当前状态与前一状态的关系。
b. 转移边
  • 定义:转移边表示从一个状态到另一个状态的路径,通常由字符触发。
  • 结构:每个状态可以有多个转移边,通过这些边可以到达其他状态。
c. 后缀链
  • 定义:后缀链是一系列状态的连接,用于表示字符串中不同长度的后缀子串之间的关系。
  • 作用:在构建自动机时,后缀链帮助维护最长公共前缀的信息。

3. 构建后缀自动机

a. 初始化
  1. 创建初始状态(init),其 len 为 -1,link 指针指向自身。
  2. 当前最后插入的字符所对应的节点为 last
b. 扩展节点
  • 步骤
    1. 创建新节点:当处理一个新的字符时,首先创建一个新的状态,并将其 len 设置为当前 lastlen + 1。
    2. 回溯检查:从当前 last 开始回溯,沿着转移边和后缀链寻找可以添加新转移边的状态。如果某个状态的子节点已经存
      在,则更新 link 指针。
    3. 维护最后公共状态:在回溯过程中,确保所有相关状态的后缀链正确。
c. 实现细节
  • 使用C++中的类或结构体来表示状态和转移边:
struct state {
    int len;
    int link;
    unordered_map<char, int> next;  // 转移边:字符到状态的映射
};

4. 核心算法

a. 延伸节点
  • 函数描述
    • 给定当前 last 状态和一个新字符 c,创建并返回新的状态。
  • 实现步骤
    1. 初始化新的状态 p,并将 p.len = last->len + 1
    2. 遍历从 last 开始的后缀链,寻找可以添加转移边的状态 q
    3. 更新 q 的子节点,确保不存在冲突。
    4. 返回新状态 p 并更新 last
b. 计算出现次数
  • 方法
    • 利用后缀自动机的性质,通过遍历所有状态并统计其 len 属性来计算不同子串的出现次数。
  • 代码示例
int countOccurrences() {
    int res = 0;
    for (const auto& state : states) {
        if (state.len != -1) {
            res += state.len - state.link;
        }
    }
    return res;
}
c. 最长公共前缀
  • 方法
    • 在两个字符串中找到最长的共同前缀。
  • 实现步骤
    1. 将问题转换为在后缀自动机中查找对应的路径。
    2. 使用回溯算法从根节点开始,沿着转移边寻找最长公共前缀。

5. 高级应用

a. 最长回文子串
  • 思路
    • 利用后缀自动机和一些辅助数据结构(如哈希表和manacher)来记录每个位置的对称信息。
    • 遍历所有可能的中心点,寻找最长的回文。
b. 多模式匹配
  • 方法
    • 将多个模式字符串合并到同一个后缀自动机中。
    • 使用自动机进行高效的多模式匹配。

6. 常见问题与调试技巧

a. 内存泄漏
  • 确保每个新创建的状态都被正确释放。
b. 指针错误
  • 仔细检查所有状态的 linknext 属性是否初始化正确。
c. 性能优化
  • 使用更高效的数据结构(如跳表或平衡二叉树)替代默认的哈希表。
  • 并行化某些不依赖顺序的计算步骤。

7.更多模型扩展

1.检查字符串是否出现
  • 题面
    • 给一个文本串 s 和多个模式串 t,我们要检查字符串 t 是否作为 s 的一个子串出现。
  • 思路
    • 我们先对 s 构建自动机。为了检查模式串 t 是否在 s 中出现,我们沿转移边从起点开始根据 t 的字符进行转移。如果在某个点无法转移下去,则模式串 t 不是 s 的一个子串。如果我们能够这样处理完整个字符串 t,那么模式串在 s 中出现过。

2.计算给定的字符串中有多少个不同的子串。
  • 题面
    • 给一个字符串 s,计算不同子串的个数。
  • 思路
    • 对字符串 s 构造后缀自动机。每个 s 的子串都相当于自动机中的一些路径。因此不同子串的个数等于自动机中以 t_0 为起点的不同路径的条数。令 dv 为从状态 v 开始的路径数量(包括长度为零的路径)。

3.所有不同子串的总长度
  • 题面
    • 给定一个字符串 s,计算所有不同子串的总长度。
  • 思路
    • 本题做法与上一题类似,只是现在我们需要考虑分两部分进行动态规划:不同子串的数量 dv 和它们的总长度 retv

4.字典序第 k 大子串
  • 题面
    • 给定一个字符串 s。多组询问,每组询问给定一个数 k,查询 S 的所有子串中字典序第 k 大的子串。
  • 思路
    • 字典序第 k 大的子串对应于 SAM 中字典序第 k 大的路径,因此在计算每个状态的路径数后,我们可以很容易地从 SAM 的根开始找到第 k 大的路径。

5.最小循环移位
  • 题面
    • 给定一个字符串 s。找出字典序最小的循环移位。
  • 思路
    • 字符串 S+S 包含字符串 S 的所有循环移位作为子串。所以问题简化为在 S+S 对应的后缀自动机上寻找最小的长度为 |S| 的路径,这可以通过平凡的方法做到:我们从初始状态开始,贪心地访问最小的字符即可。

6.第一次出现的位置
  • 题面
    • 给定一个文本串 t,多组查询。每次查询字符串 s 在字符串 t 中第一次出现的位置(s 的开头位置)。
  • 思路
    • 这种题又朴素的 O(T|t|) 的做法,但是显然过不了。考虑优化:
      我们构造一个后缀自动机。我们对 SAM 中的所有状态预处理位置 firstpos。即,对每个状态 v 我们想要找到第一次出现这个状态的末端的位置 firstpos[v]。换句话说,我们希望先找到每个集合 endpos 中的最小的元素(显然我们不能显式地维护所有 endpos 集合)。

      为了维护 firstpos 这些位置,我们将原函数扩展为 sam_extend()。当我们创建新状态 cur 时,我们令:

      firstpos(cur)=len(cur)1

      当我们将结点 q 复制到 clone 时,我们令:

      firstpos(clone)=firstpos(q)

      (因为值的唯一的其它选项 firstpos(cur) 显然太大了)。

      那么查询的答案就是 firstpos(t)|s|+1,其中 t 为对应字符串 s 的状态。


7.所有出现的位置
  • 题面
    • 问题同上,这一次需要查询文本串 t 中模式串出现的所有位置。
  • 思路
    • 利用后缀自动机的树形结构,遍历子树,一旦发现终点节点就输出。

8.最短的没有出现的字符串
  • 题面
    • 给定一个字符串 s 和一个特定的字符集,我们要找一个长度最短的没有在 s 中出现过的字符串。
  • 思路
    • dv 为节点 v 的答案,即,我们已经处理完了子串的一部分,当前在状态 v,想找到不连续的转移需要添加的最小字符数量。计算 dv 非常简单。如果不存在使用字符集中至少一个字符的转移,则 dv=1。否则添加一个字符是不够的,我们需要求出所有转移中的最小值

posted @ 2025-02-05 19:32 _Acheron 阅读(151) 评论(0) 推荐(1) 编辑
摘要: 定义: 若集合 G,在 G 上的二元运算(该运算称为群的乘法,注意它未必是通常意义下数的乘法,其结果称为积):GGG 构成的代数结构 (G,),满足: 封闭性:即 \(G\ 阅读全文
posted @ 2024-12-18 20:28 _Acheron 阅读(57) 评论(0) 推荐(1) 编辑
摘要: 这题很显然可以用贪心来解。 由于先手不动一定会让局数更少,所以先手要能动就动。 而后手一定是希望他的石子可以撞到一个障碍物上,这样先手就无法移动了,后手就可以让局数更少。 因为先手一定会能动就动,所以后手只能走到横坐标大于纵坐标的障碍物上方。那就很简单了,我们只需要统计符合特点的障碍物即可。 cod 阅读全文
posted @ 2024-12-14 22:13 _Acheron 阅读(16) 评论(0) 推荐(0) 编辑
摘要: 这题的难度不怎么好说,不过我认为还是挺简单的。 我们可以把答案看成由多个子图构成的图,这样我们只需要手打一个小子图,从中推出完整的答案。 - 把小于子图范围的地方填上子图的字母 - 如果这个点的横坐标或纵坐标小于子图范围就填上 T_{i,0} 或 T_{0,j} 详见注释 int main() { 阅读全文
posted @ 2024-12-14 22:09 _Acheron 阅读(5) 评论(1) 推荐(0) 编辑
摘要: 很显然的动态规划。 令 fi,jn=ik=j 时满足题意的集合数。 依题意可得:一个集合可以只由另一个集合添加元素或将所有元素除二得到。 初始:f0,0=1。 目标:fn,k。 所以可得: - fi,j=fi1,j1+fi,j阅读全文
posted @ 2024-12-14 22:05 _Acheron 阅读(14) 评论(0) 推荐(0) 编辑
摘要: 荆轲将会臭名昭著 首先 15 做法很简单,那就是直接 `cout<<-1` 考虑用 BFS 来解思路很简单,但是怎么求每个士兵的控制范围呢? 直接暴力时间复杂度是 O(nma2) 当然过不了一定会TLE。 所以,只需要差分+前缀和即可。 说起来简单,实现起来也简单。 然后,单打广搜大家应该 阅读全文
posted @ 2024-12-14 21:59 _Acheron 阅读(3) 评论(0) 推荐(0) 编辑
摘要: 数论题,先看数据范围,发现 nm 都非常大,但是 i=1i=nai109。 解以上不等式得不同的 ai 大约有 40000 个。记有 cnt 个不同的 ai,所以显然有一种 O(k2) 的做法。 期望得分:70 分。 阅读全文
posted @ 2024-12-14 21:53 _Acheron 阅读(3) 评论(0) 推荐(0) 编辑
摘要: 有个很显然的结论,题目中的 xy 奇偶性相同。 有个更简单的证明,奇数的平方为奇数,偶数的平方为偶数,所以 xy 奇偶性相同。 思路就显而易见了,考虑构造一个长度为 y 的序列,其中的每个数为 ±1。答案就比较显然了,我们先假设有 y1 考虑每将 阅读全文
posted @ 2024-12-14 21:46 _Acheron 阅读(5) 评论(0) 推荐(0) 编辑
摘要: 1.约数之和: 令 pkPakZ+。 则大于 1 的正整数 n 可以表示为以下形式(质因数分解)。 n=k=0mpkak 此时 n 的所有约数之和为: $\prod \limits_{ 阅读全文
posted @ 2024-12-14 21:42 _Acheron 阅读(8) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示
被光所引,为光而行。