当前位置: 首页 > news >正文

SAM exSAM 学习笔记

73d666d030ea3f9dd1c8b5c22223252a

概述 - SAM

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

其主要解决的问题是子串相关的问题。

一些符号

  • \(\Sigma\) 为字符集,\(|\Sigma|\) 为字符集大小,在代码中使用常量 SIG 表示

  • \(\text{endpos}(t)\) 表示 \(t\) 的结束位置集合。在下文中定义。

  • \(\text{endpos}(u)\) 表示等价类 \(u\) 中任意字符串的结束位置集合。

  • \(\text{link}(u)\) 表示节点 \(u\) 的后缀链接。

  • \(\text{longest}(u), \text{shortest}(u)\) 为节点 \(u\) 表示的字符串中最长 / 最短的。

  • \(\text{maxlen}(u),\text{minlen}(u)\) 为节点 \(u\) 表示的字符串中最长 / 最短字符串长度。\(\text{maxlen}\) 单独出现时也记作 \(\text{len}\)

定义 / 性质

对于字符串 \(s, |s| = n\) 的 SAM:

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

  • SAM 是一张 DAG,节点称为状态,边称为转移。转移边上标有字符。

  • 初始状态唯一且能到达所有状态。

  • 终止状态不唯一,初始状态到终止状态的路径上的转移构成一个 \(s\) 的后缀。反之每个后缀可以由初始状态到一个终止状态的路径构成。

  • SAM 的节点个数、转移边个数、构造复杂度均为 \(\mathcal{O}(n)\)。具体地,节点个数最多为 \(2n - 1\),转移边个数最多为 \(3n - 4\)

  • SAM 中实际存储了 \(s\) 的所有子串:

    • SAM 是接受 \(s\) 所有后缀的最小 DFA,也就是包含了所有后缀的前缀信息,即所有子串。

    • SAM 是 \(s\) 反串 \(s'\) 的后缀树,存储了 \(s'\) 后缀(即 \(s\) 前缀)的前缀(即 \(s\) 的前缀的后缀),即所有子串。

构成

endpos 等价类

对于一个 \(s\) 的子串 \(t\),记它在 \(s\) 中所有结束位置(每次出现是一段连续的 \(|t|\) 个字符,那么结束位置为这 \(|t|\) 个字符的末尾)的集合为 \(\text{endpos}(t)\)

若干个 \(\text{endpos}\) 相等的 \(t\),称为一个 \(\text{endpos}\) 等价类。在 SAM 中,我们将 \(\text{endpos}\) 等价类合并考虑,每个节点就是一个 \(\text{endpos}\) 等价类。

可以发现一个性质,对于一个 \(\text{endpos}\) 等价类,记其中字符串按照长度从小到大排序为 \(t_{1, \cdots, m}\)。那么 \(\forall 1 \le i \lt m\)

  • \(\text{len}(t_{i + 1}) = \text{len}(t_i) + 1\)

  • \(t_i\)\(t_{i + 1}\) 的后缀。

举例:\(s = \texttt{cdabcbcdabc}\)

  • \(\text{endpos} = \{ 5, 11 \} : \texttt{cdabc, dabc, abc}\)

  • \(\text{endpos} = \{ 5, 7, 11 \} : \texttt{bc}\)

  • \(\text{endpos} = \{ 1, 5, 7, 11 \} : \texttt{c}\)

证明平凡:

  • \(t_i\) 若不是 \(t_{i + 1}\) 的后缀,则不会同时出现在一个位置。

  • \(\text{len}(t_{i+1})\gt\text{len}(t_i)+1\),则存在一个 \(t_{i + 1}\) 的后缀 \(t'\),满足 \(\text{len}(t') \gt \text{len}(t_i)\)

    此时 \(t_i\)\(t'\) 的后缀,那么只有 \(t_i\) 出现 \(t'\) 才能出现,则 \(\text{endpos}(t') \subseteq \text{endpos}(t_i)\)

    同理,因为 \(t'\)\(t_{i + 1}\) 的后缀,\(\text{endpos}(t_{i + 1}) \subseteq \text{endpos}(t')\)

    \(\text{endpos}(t_i) = \text{endpos}(t_{i + 1})\),所以 \(\text{endpos}(t') = \text{endpos}(t_i) = \text{endpos}(t_{i + 1})\)\(t'\) 也是这个等价类里面的。

转移边 \(\text{next}(u, c) = v\) 满足 \(u\) 代表的所有字符串在末尾加入字符 \(c\) 后得到的结果在 \(v\) 表示的等价类中。
显然不可能出现这些加了 \(c\) 得到的字符串出现在两个 \(\text{endpos}\) 等价类中,不然原来的字符串就不构成等价类了。

在代码中记为 st[u].nxt[c]


对于节点 \(u\),记 \(\text{shortest}(u)\) 删去第一个字符所得的 \(t'\) 所在的节点 \(v\),定义 \(\text{link}(u) = v\)

还是举例:\(s = \texttt{cdabcbcdabc}\)

  • \(\text{endpos}(A) = \{ 5, 11 \} : \texttt{cdabc, dabc, abc}\)

  • \(\text{endpos}(B) = \{ 5, 7, 11 \} : \texttt{bc}\)

  • \(\text{endpos}(C) = \{ 1, 5, 7, 11 \} : \texttt{c}\)

\(\text{link}(A) = B, \text{link}(B) = C\)

在代码中记为 st[u].lnk

后缀链接树

容易发现,对于所有点 \(u\),将 \(\text{link}(u)\)\(u\) 连边可以得到一棵树。

性质:后缀链接树上每个点 \(u\)\(\text{endpos}(u)\) 为 $S \cup \left ( \bigcup _ {v \in \mathrm{son}(u)} \text{endpos}(v)\right ) $。

  • \(\text{longest}(u)\)\(s\) 的一个前缀时,\(S = \{ \text{len}(\text{longest}(u)) \}\)

  • 否则 \(S = \varnothing\)

证明平凡:

  • 显然 \(\text{longest}(u)\) 一定是所有出现在 \(u\) 在后缀链接树上的子树(不为 \(u\))的字符串的真后缀。

  • \(\text{longest}(u)\) 不是 \(s\) 的前缀,则一定作为真后缀出现在「「\(u\) 在后缀链接树上的子树内所有点 \(v\)\(u \not = v\))」代表的字符串」中,故 \(\text{endpos}(u) = \bigcup _ {v \in \mathrm{son}(u)} \text{endpos}(v)\)

  • 否则只会多一个作为 \(s\) 前缀出现的位置 \(\text{len}(\text{longest}(u))\)

SAM 的后缀链接树实际上是 \(s\) 反串 \(s'\) 的后缀树。\(s'\) 的后缀树是 \(s'\) 所有后缀组成的 trie 上,对这些后缀的结束节点求虚树所得。
后缀树是把后缀按照前缀对齐,SAM 后缀链接树是把前缀按照后缀对齐。

线性建 SAM

点击查看代码
const int LEN = 2e6 + 5;
const int SIG = 26;struct SAM{struct state{int len, lnk, cnt;int nxt[SIG];} st[LEN << 1];int siz = 0;SAM(){siz = 0;st[0].len = 0;st[0].lnk = -1;}int new_node(){int x = ++siz;memset(st[x].nxt, 0, sizeof(int) * 26);return x;}int clone_node(int lst){int x = new_node();st[x].lnk = st[lst].lnk;st[x].len = st[lst].len;rep(i, 0, SIG - 1){st[x].nxt[i] = st[lst].nxt[i];}return x;}int extend(int c, int lst){int x = new_node();st[x].cnt = 1;st[x].len = st[lst].len + 1;int p = lst;while(p != -1 && !st[p].nxt[c]){st[p].nxt[c] = x;p = st[p].lnk;}if(p == -1){st[x].lnk = 0;return x;}int q = st[p].nxt[c];if(st[p].len + 1 == st[q].len){st[x].lnk = q;}else{int cl = clone_node(q);st[cl].len = st[p].len + 1;while(p != -1 && st[p].nxt[c] == q){st[p].nxt[c] = cl;p = st[p].lnk;}st[q].lnk = st[x].lnk = cl;}return x;}vector<int> tr[LEN << 1];void build_tr(){rep(x, 1, siz){tr[st[x].lnk].push_back(x);}}
} SAM;

算法流程(extend)函数:

  • 传入字符 \(c\)(编码为数字,范围 \([0, |\Sigma|)\))和拓展节点 \(lst\)
    代表 extend 函数会给 \(lst\) 节点构造出 \(nxt_c\) 的节点并保证所有点的指针全部正确。

  • 新建节点 \(x\)\(\text{len}(x) \leftarrow \text{len}(lst) + 1\)

  • 由于 \(x\) 的加入,更新一些原本为空的 \(nxt_c\)

    \(p \leftarrow lst\)。循环:

    • \(\text{next}(p,c)\) 未定义,赋值为 \(x\)

    • 否则,跳出循环。

    显然满足了对 \(\text{next}\) 指针的定义。

  • \(x\) 节点找 \(\text{link}\),也就是找到 \(\text{longest}(x)\) 的一个最长后缀满足不与 \(\text{longest}(x)\) 在一个等价类里。
    相当于找到 \(\text{longest}(x)\) 的一个已经出现的最长后缀,记为 \(t\)

    • 若此时 \(p\) 已经跳没了,即跳到了初始状态,则特判掉,\(\text{link}(x)\) 赋值为初始状态。

    • 否则,\(t\) 就是 \(\text{longest}(p)\) 添加 \(c\) 得到,就在 \(q = \text{next}(p, c)\) 中。
      这里是因为跳到这个 \(p\) 以前经过的节点都没有 \(\text{next}(...,c)\),说明其最长串加上 \(c\) 得到的串(该串为 \(\text{longest}(x)\) 的后缀)未曾出现。

      • \(\text{len}(q) = \text{len}(p) + 1\),即 \(\text{longest}(q)\) 恰好为 \(t\)
        直接令 \(\text{link}(x) \leftarrow q\) 即可。

      • 否则,说明 \(q\) 中存在一些字符串长度大于 \(t\),显然这些字符串不会是 \(\text{longest}(x)\) 的子串。
        那么我们发现,\(q\) 这个等价类就出现了分化:长度小于等于 \(t\) 的字符串的 \(\text{endpos}\) 会新增,其他不变。
        也就是说,等价类 \(q\) 已经解体。

        我们克隆一个节点 \(cl\),继承 \(q\) 除了 \(\text{len}\) 以外的 \(\text{next}\) 指针和 \(\text{link}\) 指针。
        让它的 \(\text{len}_{cl} \leftarrow \text{len}_p + 1\),代表原来 \(q\) 中长度小于等于 \(t\) 的。
        然后执行 \(\text{link}(q), \text{link}(x) \leftarrow cl\)

关于复杂度:每次最多建立两个新节点 \(x, cl\),且第一次调用 extend 不可能新建 \(cl\),故节点数 \(\le 2n - 1\)

http://www.jsqmd.com/news/298859/

相关文章:

  • Matlab去除CT扫描图像环形伪影的实现方法
  • 《把脉行业与技术趋势》-100-电动机——永不落幕的能源转换艺术
  • 麻省理工学院人工智能领域有影响力人物
  • 幽冥大陆(一百11)酒店智能门锁系统Larkdll接口函数——东方仙盟筑基期
  • 未来之窗昭和仙君(六十三)可编程子窗口操作功能—东方仙盟练气期
  • 基于开源AI大模型S2B2C商城系统的无人店铺售卖难点解决方案研究
  • Linux驱动学习笔记:spi-imx.c收发消息的核心流程
  • 内核日志分析:__spi_pump_messages的Caller_Optimization和KWorker_Thread
  • 2025年市场上优质的非标钣金定制企业哪个好,行业内专业的非标钣金定制哪个好睿意达诚信务实提供高性价比服务
  • Flutter for OpenHarmony:从零开始认识基础组件
  • 得物Java面试被问:RocketMQ的消息轨迹追踪实现
  • 期货交易平台数据分析系统开题报告
  • 基于大数据+Hadoop的智能电网环境下的电能质量监测系统开题报告
  • 我的ppo转头找门模型成功收敛 当它发现原地不动有分的时候
  • 浙江洁净车间厂家深度评测:百级洁净度的关键考量,净化工程公司/无尘室/无尘车间/净化工程/净化车间,洁净车间施工哪家靠谱
  • 2026年重庆等地值得选的安全阀在线检测仪服务商排名
  • 2026年电镀金加工推荐厂家怎么选,鼎亚电子优势明显
  • 2026年金属带材电镀生产企业排行榜,十大厂家有谁?
  • 2026年慢走丝机床品牌排名揭晓,上海汉霸数控以硬核实力上榜!
  • 成都婚纱摄影怎么选?2025-2026年度真实口碑排行榜单
  • 2026年润滑油泵服务商厂家排名揭晓,宁波迪奥机械名列前茅!
  • 2026年连续镀专业供应商,无锡鼎亚电子实力如何?
  • 2026年湖北电镀金灵活化加工,性价比高的厂家排名揭晓
  • 2026年探寻汉霸地区靠谱的火花机设备供应商
  • 青城腊香传千年: 青城山小农哥川味特产店-成都江堰打卡新地标
  • 计网 01 WebSocket | MDN - 教程
  • 2026年顺德有名的刀塔机采购排行,三轴机/4+4车铣/双主轴双刀塔/插补Y/36排刀机/Y轴/四轴机,刀塔机工厂找哪家
  • 2026年口碑好的管家婆软件服务商排行榜,用友 T3/好会计/税务云/协同云/易代账/好生意,管家婆软件系统推荐榜单
  • 山东临沂玻璃门工厂推荐:顶立固为何成为3000+B端客户的共同选择?
  • Redis 高可用进阶(一):主从复制核心逻辑全解析