第 5 章 · 03 SAM 后缀自动机难点精讲 ★ 本节定位:对应 OI Wiki 。难度:高阶(省选/IOI)。前置依赖:第 5 章 · 02 节字符串基础(尤其 KMP 的 fail 思想、Trie)、树形结构(parent tree 是棵树)。本节是难点精讲节(★),对 SAM 做 Wiki 之外的增量慢节奏讲解,作 的配套讲义,不重写 Wiki。 ⚠️ 注意:SAM 被公认为"字符串算法的顶峰"。Wiki 页面写得已经很全,但概念密度极高。本节的任务是把这些概念拆开、排序、画图,让你先建立直觉再回 Wiki 看证明。
本节定位:对应 OI Wiki
docs/string/sam.md。难度:高阶(省选/IOI)。前置依赖:第 5 章 · 02 节字符串基础(尤其 KMP 的 fail 思想、Trie)、树形结构(parent tree 是棵树)。本节是难点精讲节(★),对 SAM 做 Wiki 之外的增量慢节奏讲解,作docs/string/sam.md的配套讲义,不重写 Wiki。
⚠️ 注意:SAM 被公认为"字符串算法的顶峰"。Wiki 页面写得已经很全,但概念密度极高。本节的任务是把这些概念拆开、排序、画图,让你先建立直觉再回 Wiki 看证明。
SAM 之所以劝退,是多个抽象概念同时交织,没有任何一个可以单独跳过:
last 指针,分两种情况——其中 clone(克隆节点)是最难理解的一步。这四者环环相扣:不懂 endpos 就不懂状态,不懂 parent tree 就不懂 link,不懂 link 就看不懂构建算法里的 clone。所以本节按"定义 → 概念1→2→3→4 → 构建算法 → 应用"的顺序逐层加概念。
字符串 s 的 SAM 是一个能识别 s 所有子串的最小 DFA(确定性有限状态自动机)。
对子串 t,endpos(t) = t 在 s 中所有结束位置的集合(下标从 0)。
例:s = abcbc,则 endpos("bc") = {2, 4}(在第 2、4 位结束)。
关键观察:两个子串 endpos 相同 ⟺ 它们在 s 中"总是形影不离"地出现。
按 endpos 是否相同,把 s 的所有子串分成若干等价类。
💡 关键事实:SAM 的每个状态 = 一个 endpos 等价类(再加一个初始状态)。这是 SAM 把 Θ(n²) 压到 O(n) 的根本原因——等价类个数是线性的。
引理(直觉版):同一等价类里的子串,长度连续,且"长的包含短的"——它们互为后缀。
不同等价类的 endpos 集合有包含关系:endpos 大的类,对应子串更短。把这些包含关系组织成一棵树,就是 parent tree。
link(v):状态 v 的后缀链接,指向"endpos 真包含 v 的那个最紧的等价类"。⚠️ 注意:parent tree 的方向是初学者最易搞混的点。记住口诀:"沿 link 向上,串变短、endpos 变大;根在最上面,endpos 最大。" 别画反。
每个状态 v 有一条 trans[v][c]:从 v 读入字符 c 后到达的状态。这张转移图就是 SAM 本体(一张 DAG)。
一个状态要维护三个东西:len(该类最长子串长度)、link(后缀链接)、trans(转移字典)。
维护 last = 当前整个已加入前缀所在的状态。每加入一个字符 c,分两种情况:
Case 1(简单情况):新建状态 cur,len[cur] = len[last] + 1。从 last 沿 link 往上走,凡是没有 c 转移的,都把 trans 指向 cur;若走到根都没冲突,直接 link[cur] = t0。
Case 2(需要 clone,最难):从 last 沿 link 往上走时,遇到一个状态 p 已有 c 转移到 q。这时要判断:
len[q] == len[p] + 1:直接 link[cur] = q,结束。len[q] > len[p] + 1(q 还代表更长的串):必须 clone——把 q 复制成 clone,len[clone] = len[p] + 1,继承 q 的 trans 和 link,然后让 q 和 cur 的 link 都指向 clone,再把 p 这条链上原本指向 q 的 c 转移改指向 clone。💡 clone 为什么必须:q 原本"身兼两职"(既代表长串又代表短串),加入 cur 后这两种身份必须分开,否则转移会出错。clone 就是把"短串身份"剥离出来。这一步没有直觉捷径,只能对着 Wiki 的例子(如
aab、ababa)手推。
构造的总复杂度是 O(n)(字符集视为常数),这是 SAM 比后缀数组 SA"在线 + 线性"的优势所在。
len[v] - len[link[v]] 个新子串,求和即得。| 维度 | SAM | SA(后缀数组) |
|---|---|---|
| 构建方式 | 在线逐字符 | 离线排序所有后缀 |
| 复杂度 | O(n) | O(n log n) 或 O(n) |
| 核心结构 | parent tree + trans | sa 数组 + height 数组 |
| 强项 | 子串计数、多模式 | LCP、重复子串 |
两者解决大量重叠的问题,但 SAM 在线构建、状态线性,更适合"动态加字符"的场景。
len[q] > len[p] + 1 这个判断写全。[len[link[v]] + 1, len[v]],不是只有 len[v] 一个。aab、ababa 的构建全过程(每加一个字符画一张 SAM),再写代码。没有手推直接写,几乎必然调一晚上。💡 学习提示:SAM 是"看懂证明 ≠ 会写代码"的典型。建议先抄一遍 Wiki 的模板代码 AC 模板题,再回头理解每个分支的作用,最后才尝试自己从零写。