SAM 略解
前言
只要你愿意啃,没有算法是学不来的
——教练
说实话,学完 SA 后有时间都会去看 SAM ,但就是怀着信息去,带着一脑子问号回来
根据教练の哲理,一定要把 SAM 啃下来
引入
后缀自动机能解决很多问题。
举个例子
- 在一个字符串中搜索另一个字符串所有出现位置
- 得到有多少本质不同的字串
当然,这些 SA 也能轻易完成
但是 SAM 能做到很多 SA 做不到的 它最大的优势是在线的
而且它有十分优秀的时间复杂度
SAM 的定义
- SAM 是一张有向无环图 只有一个原点 称为初始状态 其它所有点都可以通过 到达
- 每条边都是一个转移 每个节点都是一个状态
- 每个状态的转移都是不同的
- 存在多个终止状态,满足从原点 出发,到达终止状态立得到的字符串的必是原字符串的后缀 并且满足任何 的后缀都可以由一条路径表示
- 满足上述所有条件前提下,SAM 满足节点数最少
SAM 的基本性质
SAM 保证:
- 字符串中的任意一个字串都可以由一条路径表示
- SAM 任意一个节点表示的字符串都是原串 的字串
一些例子
可以前往 OI-wiki 查看
一些重要的概念
endpos
定义:我们记 表示字符串 在原串 中所有出现结束位置的集合。
举个栗子,对于 则 ,
对于两个字串 , ,若满足 我们认为 是一个等价类。
因此, 里面所有的字串可以分成若干个等价类。
而我们 SAM 中的节点,就是所有等价类带边的点
一些定理/引理
- 若字符串 满足 为 的后缀
这个感性证明是最好的,自行理解就行
- 每个等价类包含的字符串长度一定都是在区间 之间的
也是推荐感性理解,长度 以下的是当前集合的超集, 以上的是当前集合的子集
这个区间内一定都是连续的数字,因为字串是连续的
不理解可以看看例子
对于任意两个子串
如果 为 的后缀,那么它们满足
否则满足
根据 可以推证
后缀链接 link
先看张图
红色的边就是后缀链接
其实用最直白的话来讲 后缀链接就是当前等价类最短后缀的下一个后缀 就是当前等价类的超集
比如说 它的最短后缀是 那么它连接到的就是 因为
还是很好理解的 手画一下其它节点都满足这个性质
- 所有 构成一棵节点为 的树
考虑对于任意节点往后缀链接移动,能满足当前等价类的长度不断变短,最终总能到达
根据 可以证明不存在环
所有点联通不存在环的无向图就是树
对于这棵树 我们称为
-
对于任意状态,记它的长度区间为 那么对于任意节点 满足 =
-
对于任意状态 向着 遍历到 经过所有状态长度区间的交集为
即从任意状态往 走,可以完成遍历完它的所有后缀
我们 SAM 的状态点与 parent tree 的状态是完全相同的
对于一整个字符串 它只会在 parent tree 中分裂 次 所以可以保证节点在最坏条件下是 的
知道这些后,我们可以开始构造 SAM 了
SAM 的构造
SAM 是一个不停往后加字符的在线算法
SAM 是和 parent tree 同时构造的
构造流程 (PS:一定要在纸上边画图边写):
现在我们要加入字符
-
定义 表示添加字符 之前的节点
-
创建一个新节点 表示当前节点 令
这一句话的意思是当前等价类的最长长度是上一个的 非常显而易见的性质 -
从 开始遍历 并向当前节点加一条 的出边
我们知道 一直跳 可以遍历完所有后缀 这个过程相当于向每个后缀加一条出边 -
如果跳到了原点 令 转
为什么呀?为什么?
一个点跳到原点都没有 的出边 意味着遍历了当前所有后缀的前缀都没有 所以 是第一个出现的字符 直接把 设为原点即可 -
如果当前节点 有一条出边 指向 ,我们开始分类讨论
-
如果 直接把 即可 转 8
为什么呀?为什么?
首先 我们先说明 因为 是 转移来的 这是非常显然的
然后 相等是一个特殊情况
我们考虑为什么要给等价类连边
不就是在当前字符串满足后缀长度最小时 前面有相同部分出现吗
那么 把后缀同时去掉一个 求得的也是相同的
也就是说 得到的就是 的最长开始重复部分 也就是下一个等价类 刚好满足 直接连接即可 转 -
现在情况有些复杂
因为我们通过 的分析 是可以直接 的 但是不等却不行
为什么呢 因为这样跳上去的节点存在部分不是 的后缀
这个时候我们直接把这个节点分裂成两个 和 令
然后把 的 设为
再把 的 设为 即可
这样点之间的我们就处理完毕了 考虑边之间的关系
注意开始 和 的出边也是相同的 思考一下这是对的
接着就是入边
我们发现入边情况就多了 要分成连向 和 两种情况分类讨论
当然一开始还是所有边都连向 现在考虑拆边
我们考虑为什么有边要连向 因为它比 长度小
也就是说 只要从 继续往上跳 跳到有出边 而且长度再 区间内 直接不连 连 即可
否则如果连的不是 ,而是 的祖先 说明这些是新的等价类 跳出循环即可
根据往后后缀长度或越来越短 我们保证肯定每个后缀都会有 的出边
所以一直操作即可 转 -
令 转
这样就完成了
时间复杂度
是线性的 我们认为是 的 是字符集的大小
根据代码 我们知道 SAM 只有 个节点 条边
构建结束标记
我们只需要在当前最后状态往上跳 就能遍历完最大后缀
然后结束标记都是在最后在最后结束的 直接打标记即可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)