【学习笔记】SAM的结构和应用

Oi-wiki

让我们从头说起。字符串 s s s S A M SAM SAM是一个接受 s s s的所有后缀的最小 D F A DFA DFA。既然是 D F A DFA DFA那么就存在一个或多个终止状态,如果我们从初始状态 t 0 t_0 t0出发,最终转移到了一个终止状态,则路径上的所有转移连接起来一定是字符串 s s s的一个后缀。反过来, s s s的每个后缀均可用一条从 t 0 t_0 t0到某个终止状态的路径构成。

S A M SAM SAM包含关于字符串 s s s的所有子串信息。任意从初始状态 t 0 t_0 t0开始的路径,如果我们将转移路径上的标号写下来,都会形成 s s s的一个子串。反之每个 s s s的子串对应从 t 0 t_0 t0开始的某条路径。唯一需要注意的是若干个子串可能对应同一条路径,因为是去重过后的。

考虑字符串 s s s的任意非空子串 t t t,记 endpos(t) \text{endpos(t)} endpos(t)为在字符串 s s s t t t的所有结束位置。这样所有字符串 s s s的非空子串都可以根据它们的 endpos \text{endpos} endpos集合被分为若干个 等价类

引理1:字符串 s s s的两个非空子串 u u u w w w(假设 ∣ u ∣ ≤ ∣ w ∣ |u|\le |w| uw)的 endpos \text{endpos} endpos相同,当且今当字符串 u u u s s s中的每次出现,都是以 w w w的后缀形式存在的。

引理2:考虑两个非空子串 u u u w w w(假设 ∣ u ∣ ≤ ∣ w ∣ |u|\le |w| uw),那么如果 u u u w w w的一个后缀,则 endpos ( w ) ⊆ endpos ( u ) \text{endpos}(w)\subseteq \text{endpos}(u) endpos(w)endpos(u);否则 endpos ( w ) ∩ endpos ( u ) = ∅ \text{endpos}(w)\cap \text{endpos}(u)=\empty endpos(w)endpos(u)=

引理3:考虑一个 endpos \text{endpos} endpos等价类,将类中的所有子串按长度非递增的顺序排序。那么对于同一等价类的任意两子串,较短者为较长者的后缀,且该等价类中的子串长度 恰好覆盖整个区间 [ x , y ] [x,y] [x,y]

这些东西就不用证了吧。。。

考虑 S A M SAM SAM中某个不是 t 0 t_0 t0的状态 v v v。我们已经知道,状态 v v v对应具有相同 endpos \text{endpos} endpos的等价类。我们如果定义 w w w为这些字符串中最长的一个,则所有其他的字符串都是 w w w的后缀。定义一个后缀连接 link(v) \text{link(v)} link(v)连接道对应于 w w w最长后缀的另一个 endpos \text{endpos} endpos的等价类状态。

为了方便,规定 endpos ( t 0 ) = { − 1 , 0 , . . , ∣ S ∣ − 1 } \text{endpos}(t_0)=\{-1,0,..,|S|-1\} endpos(t0)={1,0,..,S1}

引理4:所有后缀链接构成一颗根节点为 t 0 t_0 t0的树。唯一需要注意的是这颗树上的节点也对应这个 D A G DAG DAG上的点。证明也非常简单,考虑每次会连接到长度更短的后缀,最后总能到达空串对应的初始状态 t 0 t_0 t0

引理5:通过 endpos \text{endpos} endpos集合构造的树(每个子结点的 s u b s e t subset subset都包含在父节点的 s u b s e t subset subset中)与通过后缀连接 link \text{link} link构造的树相同。换句话说,后缀连接构成的树本质上是 endpos \text{endpos} endpos集合构成的一棵树。事实上仔细观察一下还会发现一个节点的 endpos \text{endpos} endpos就是所有子结点的 endpos \text{endpos} endpos集合的并。关于任意节点的 endpos \text{endpos} endpos集合怎么求我们后面会提到。

然后介绍一下 S A M SAM SAM的构造过程。

考虑给当前字符串添加一个字符 c c c的过程。

  • l a s t last last为添加字符 c c c之前,整个字符串对应的状态。
  • 创建一个新的状态 c u r cur cur,并将 len ( c u r ) \text{len}(cur) len(cur)赋值为 len ( l a s t ) + 1 \text{len}(last)+1 len(last)+1,此时 link ( c u r ) \text{link}(cur) link(cur)还未知。
  • 从状态 l a s t last last开始,如果还没有到字符 c c c的转移,那么就添加一个道状态 c u r cur cur的转移,遍历后缀链接。如果在某个点已经存在到字符 c c c的转移,我们就停下来,并将这个状态标记为 p p p
  • 如果没有找到这样的状态 p p p,我们就到达了虚拟状态 − 1 -1 1,将 link ( c u r ) \text{link}(cur) link(cur)赋值为 0 0 0并退出。
  • 假设现在我们找到了一个状态 p p p,其可以通过字符 c c c转移。将转移到的状态标记为 q q q。事实上此时我们已经知道 c u r cur cur的后缀链接的长度应该为 len ( p ) + 1 \text{len}(p)+1 len(p)+1了。所以我们只需要进行一些修正即可。
  • 如果 len ( p ) + 1 = len ( q ) \text{len}(p)+1=\text{len}(q) len(p)+1=len(q),那么只要将 link ( c u r ) \text{link}(cur) link(cur)赋值为 q q q并退出。
  • 否则创建一个新的状态 c l o n e clone clone,复制 q q q的除 l e n len len外的所有信息(后缀链接和转移)。将 len ( c l o n e ) \text{len}(clone) len(clone)赋值为 len ( p ) + 1 \text{len}(p)+1 len(p)+1。注意这个时候应该是 c l o n e clone clone q q q c u r cur cur的后缀,所以将后缀链接从 c u r cur cur指向 c l o n e clone clone,也从 q q q指向 c l o n e clone clone。最后还要完成一个重定向的过程。使用后缀链接从状态 p p p往回走,只要存在一条通过 p p p到达 q q q的转移,就将该转移重定向到状态 c l o n e clone clone。这也很好理解,因为 c l o n e clone clone q q q的区别就在于 c l o n e clone clone endpos \text{endpos} endpos集合里面多了一个位置,因为是从 p p p往回走所以转移到的状态长度肯定不会超过 c l o n e clone clone,所以应该和 c l o n e clone clone放在同一个等价类当中。
  • 最后将 l a s t last last的值更新为状态 c u r cur cur

有一点复杂,但是如果多画图还是可以理解的。

事实上 S A M SAM SAM的节点个数不超过 2 n − 1 2n-1 2n1,转移数不超过 3 n − 4 3n-4 3n4。有构造能卡到上界,这里就不赘述了。我更喜欢将转移数看 D A G DAG DAG中的边数,这似乎在提示我们可以直接在 D A G DAG DAG上做文章。

简单讲几个比较基础的应用吧。

检查字符串是否出现:直接根据模式串 P P P的字符进行转移即可。但是我要提的是,这个算法还可以找到 P P P在文本串中出现的最大前缀长度。

不同子串个数:转化成不同路径条数,直接在 D A G DAG DAG上统计即可。当然直接对每个节点对应的子串数目求和也是可以的。

字典序第 K K K大串:利用路径和子串的对应关系不难贪心求出答案。但是我要提的是,如果相同的子串算出现多次,那么我们可以通过递推求出每个节点对应的 endpos \text{endpos} endpos集合的大小,相当于给每个子串赋了一个权值,可以类似计算。

最小循环移位:发现 S + S S+S S+S包含字符串 S S S的所有循环移位作为子串,那么问题等价于找一条长度为 n n n的字典序最小的路径,这显然可以贪心解决。

所有出现位置:说白了就是要找到所有节点对应的 endpos \text{endpos} endpos。可以这样来想,如果一个子串的 endpos \text{endpos} endpos集合中包含 i i i,那么说明这个子串是以 i i i结尾的前缀的后缀,换句话说在后缀树上是 i i i对应的终止节点的祖先,那么就在 i i i对应的终止节点上插入 i i i,然后线段树合并即可。显然这里也可以用上可持久化数据结构做到在线。

最短的没有出现的字符串:这个可以通过 D P DP DP解决。就不再赘述了。

两个子串的最长公共前缀:这个也非常简单。将 S S S反转一下,问题变成了求公共后缀,而根据分析我们知道这个长度就是后缀树上 L C A LCA LCA对应的 len \text{len} len

不得不感叹, S A M SAM SAM真是字符串工具的集大成者。之前没认真学真是太可惜了。

例题:CF700E Cool Slogans

CF1098F Ж-function (其实就是套了一个 S A M SAM SAM的壳子)

[LNOI2022] 串 (个人感觉最妙的一道 S A M SAM SAM题)

[NOI2018] 你的名字(用 S A M SAM SAM可以做类似子串匹配的过程)

51nod #1600 Simple KMP(增量法,以及理解贡献计算方式)

upd on 2023/10/27:添加了几道例题。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你好!关于学习数据结构的C语言笔记,我可以给你一些基本的指导和概念。数据结构是计算机科学中非常重要的一门课程,它涉及存储和组织数据的方法。C语言是一种常用的编程语言,很适合用于实现各种数据结构。 下面是一些数据结构的基本概念,你可以在学习笔记中包含它们: 1. 数组(Array):一种线性数据结构,可以存储相同类型的元素。在C语言中,数组是通过索引访问的。 2. 链表(Linked List):也是一种线性数据结构,但不需要连续的内存空间。链表由节点组成,每个节点包含数据和指向下一个节点的指针。 3. 栈(Stack):一种后进先出(LIFO)的数据结构,类似于装满物品的箱子。在C语言中,可以使用数组或链表来实现栈。 4. 队列(Queue):一种先进先出(FIFO)的数据结构,类似于排队等候的队伍。同样可以使用数组或链表来实现队列。 5. 树(Tree):一种非线性数据结构,由节点和边组成。每个节点可以有多个子节点。二叉树是一种特殊的树结构,每个节点最多有两个子节点。 6. 图(Graph):另一种非线性数据结构,由节点和边组成。图可以用来表示各种实际问题,如社交网络和地图。 这只是数据结构中的一些基本概念,还有其他更高级的数据结构,如堆、哈希表和二叉搜索树等。在学习笔记中,你可以介绍每个数据结构的定义、操作以及适合使用它们的场景。 希望这些信息对你有所帮助!如果你有任何进一步的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值