字符串

SAM

算法学习笔记(85): 后缀自动机

有清晰的图片

endpos

SAM 通过串的结束位置来将字符串划分为若干个等价类

不同 endpos 间要么包含,要么不交,endpos 间的包含关系体现为字符串间的后缀关系

根据 endpos 间的包含关系建出 parent 树,那么一个子串的所有后缀都按长度 依次 排列在该子串对应节点的祖先链上(每个 endpos 内串长连续)这条祖先链是对该子串的一个区间划分

由于每个出现位置对应一个前缀,所以 endpos 也可看作是节点子树内前缀节点的集合

因此,可以通过线段树合并维护 endpos

可以看出,SAM 得到子串的思路是 “前缀的后缀是子串”

parent tree

对于 parent tree 上的边 \((u, v)\)\(u\)\(v\) 的父亲,那么

  1. \(E_v \subseteq E_u \Longleftrightarrow str_u \sqsupseteq str_v\)
  2. \(len_v > len_u,\ \mathrm{minlen}_v = len_u + 1\),可以用这个找拓扑序,同时也是 DAG 的拓扑序

同时,parent tree 也是反串的后缀树,因为后缀树中,一个节点的子树中叶子节点的集合对应这个串的开始位置的集合

节点个数

由于在 parent tree 上,我们每往儿子方向走一层,endpos 就被划分成若干个不相交的子集,所以

转移边(DAG)

如果 \(s\)\(S\) 的子串,\(s + c\) 依然是 \(S\) 的子串,那我们称 \(s\)\(c\) 这一条转移边,对应到 endpos 集合上,就是 SAM 的转移边

对于一条转移边 \(u \stackrel{\text{c}}{\rightarrow} v\),显然有

\[E_v = \{x + 1 | x \in E_u \land s_{x+1} = c\} \]

\[len_v \ge len_u + 1 \]

每个子串都与 DAG 上从起点开始的一条路径一一对应,所以子串问题也可以通过 DAG 上 DP 解决

GSAM

对于多串的情况,把这些串插入字典树,将 endpos 里面的位置拓展为字典树上的节点,构造时从字典树父亲的位置开始构造(可以采用 bfs 实现),或者,也可以不建出字典树,每插入完一个串后就把 las 重设为 \(1\)并在函数中加入特判

这样就有了广义 SAM

常见思路

  1. 树上倍增找子串对应节点
  2. 线段树合并维护 endpos
  3. \(\mathrm{LCS}(u, v) = len(\mathrm{LCA}\{u, v\})\)
  4. DAG 上 DP
  5. 把 SAM 当作 AC 自动机去匹配字符串
    双指针,设在 A 上匹配 B,现在尝试匹配 \(B[l, r]\),如果匹配不上就跳 link,并且 l++ 则可以得到 B 在 \(r\) 处能匹配的最长后缀

字符串间的关系在 SAM 上的刻画

  1. \(A\)\(B\) 的后缀 \(\Longleftrightarrow\) \(A\) 在 parent tree 上是 \(B\) 的祖先
  2. \(A\)\(B\) 的前缀 \(\Longleftrightarrow\) 在 SAM 上存在路径 \(A \to B\)
  3. \(A\)\(B\) 的子串 \(\Longleftrightarrow\) 将 parent tree 视为外向树(父亲指向儿子),存在路径 \(A \to B\)
    \(\Longleftrightarrow\) 对于 \(B\) 的任意一个 endpos \(x\)\(E_A \cap [x - l_B + l_A, x] \neq \emptyset\)

对比 SAM 与 ACAM

转移边 \(S \rightarrow S+c\) fail/link \(S \to P\)
SAM \(S+c\) 仍然是文本串子串 \(P\)\(S\) 的后缀,且 endpos 包含 \(S\) 的 endpos
ACAM \(S+c\) 仍然是模式串前缀 \(P\)\(S\) 的后缀,且 \(P\) 也是某模式串的前缀

在匹配过程中,ACAM 始终保持当前匹配的模式串是能匹配上的最长前缀

SA

相较于 SAM 来说,SA 更擅长处理字典序相关问题

SA 的结构

可以看作维护出来了 \(s\) 后缀构成的 Trie 上每个后缀结尾结点的 dfn 序(按照字符大小访问出边)

\(height\) 数组就是相邻两个结点的 \(\mathrm{LCA}\) 深度

你可以通过 SA 找出一个字符串的所有子串(后缀的前缀是子串),每个子串都对应上面 Trie 中的一个节点,自然,包含它为前缀的后缀在 SA 中是连续的

据此,我们可以建出这些节点的虚树,但是更便捷的方法是建出 \(height\) 数组的笛卡尔树(底部(当笛卡尔树节点对应区间为 \([l, l]\))挂后缀自己(长度)),由于 \(height\) 本质是 \(\mathrm{LCA}\) 深度,而串长也是深度,仔细思考会发现其结构与虚树大致相同(弦论

我们也可以将 SA 与 并查集 联系,每个 \(sa[i]\)\(sa[i - 1]\) 连一条边权为 \(ht_i\) 的无向边,然后从大到小加入边,设此时已经加入所有 \(\ge k\) 的边,那么每个连通块中的点对的 \(lcp\)\(\ge k\)

SA 的应用

  1. 本质不同字符串个数
  2. 文本固定,模式在线给出的匹配
  3. 比较子串大小
  4. \(k\) 小子串
  5. 连续相同子串([NOI2016] 优秀的拆分's Trick)

字符串匹配

BM 神大人的笔记

%%% Bonely_Muffin

给定两个串 \(A\)\(B\),对于 \(A\) 的每个前缀 \([1, i]\) 求其最长的后缀 \([j, i]\),满足 \([j, i]\)\(B\) 的前缀

KMP

给定两个串 \(A\)\(B\),对于 A 的每个前缀 \([1,i]\),求其在 \(B\) 中出现的最长后缀长度

(你的名字)

  1. 在 SAM 上匹配,可以双指针
  2. 相当于求 \(B\) 的所有前缀与 \(A[1, i]\) 的 LCS 的最大值,可以利用后缀数组解决

给定串 \(A\)\(B\),对于每个 \(A\) 的前缀 \([1,i]\),求出有多少个二元组 \((j,l)\) 满足 \(A[j,i]=B[l,l+(i−j)]\)

  1. 相当于求 \(A[1, i]\)\(B\) 的所有前缀的 LCS 之和,可以用 SA/后缀树(SAM) 解决
  2. 依旧对 A 匹配 B(在 A 的 SAM 上双指针跑 B),匹配到一个节点时,会对其 endpos 集合内(子树内)每个前缀位置造成当前匹配长度的贡献

例题

P9482 [NOI2023] 字符串

原题题意在 SA 上等价于下面的条件,下面 \(S,R\) 分别代表正串,反串(为了方便,反串的下标表示与正串一样)

\[rk_{S_i} < rk_{R_{i+2l-1}} \]

\[lcp(S_i, R_{i + 2l - 1}) < l \]

第一个限制条件是容易满足的,考虑第二个限制,我们发现 SA 并不擅长处理 lcp \(< k\) 的限制,考虑转换不等号方向,变成大于等于

这时我们发现第二个条件等价于 \(S[i, i+l-1] = R[i + l, i + 2l - 1]\),考虑利用回文串解决,统计满足条件的回文中心个数,设回文中心 \(c\) 的半径为 \(r\),那么 \(S[c - r, c - 1] = R[c, c + r - 1]\),第一个条件等价于字符 \(S[c - r - 1] > S[c + r]\),所以只用统计满足这一条件的回文中心即可

P4770 [NOI2018] 你的名字

复习串串的时候发现原来写的代码复杂度假了...

考虑在 \(S\) 上匹配 \(T\),即求出 \(T\) 的每个前缀 \(T[1,i]\)\(S\) 中匹配的最长后缀长度,设其为 \(p_i\) 这个匹配可以通过双指针求出

然后考虑 \(T\) 的 SAM 上的每一个节点 \(i\),记录它的一个出现位置 \(x\),然后它对答案的贡献就是 \(max\{0, len_i - max(len_{fa_i}, p_x)\}\)

posted @ 2026-07-07 09:28  zhm0725  阅读(18)  评论(0)    收藏  举报