后缀自动机
初学建议看这个 基本上是最好的中文资料了。
字符串的SAM是一个dAg,它的边上会有一个字符,从根节点走到终止节点会构成一个后缀。它可以且只可以接受$s$的所有后缀,并且满足可以接受$s$的后缀的自动机中,SAM是节点最少的。
由于后缀自动机是个dAg,它的每个节点代表的是一些字符串(因为并不只有一条路可以到达)。
定义 : endpos 集合
对于一个$s$的非空子串$t$,endpos(t) 表示$s$中出现的所有$t$的结束位置。
定义:endpos 等价类
如果两个字串$t_1$,$t_2$的 endpos 集合相等,那么称他们在一个$endpos$等价类。
那么所有字串都可以被分成很多的 endpos 等价类。最终构建SAM后其实所有除了根的点就代表了一个 endpos 的等价类,SAM的点数其实就是 endpos 等价类的数量+1。
前面说过,再DAG上一个点代表了一些字符串,这些字符串是通过不同路径走到这个点生成的,而这些字符串就属于一个 endpos 等价类。
引理1:字符串的两个非空子串$u,v(|u|\leq|v|)$如果他们的 endpos 等价类相等那么可以推出$u$是$v$的字串。
这个证明比较显然的。
同时可以有 若$u , v$endpos 集合相同且$|u|=|v|$那么
引理2:字符串的两个非空字串$u,v$它们的 endpos 集合要么互相包含,要么没有交。
而这两种情况取决于它们是否有子串关系。
并且有 如果$u$是$v$后缀,那么$endpos(u) \sube endpos(v)$比较显然。
引理3:把一个 endpos 等价类的串从长到短排序,那么长度一定是连续的。
也比较显然吧,证明就不写了。
我们知道了, SAM 的每一个节点代表的是一个 endpos 等价类,包含了很多串。由前面的引理我们也知道,它的所有串的长度是连续的。现在,我们定义一个点的后缀连接 link ,$link(u)$连接向一个 endpos 等价类,这个等价类包含了最长的串$t$使得$t$是$u$中最短串的子串,并且它的 endpos 集合和这个点的 endpos 集合不同。如果我们认为$len(u)$为$u$节点的最长的串的长度,$minlen(u)$表示$u$节点最短串的长度,且$t$所在的等价类对应的节点是$f$那么$len(f) + 1 = minlen(u)$。
同时为了方便,我们认为$t_0$也就是根节点,它的 endpos 集合是全集,规定为$\{0,1,\dots,|S|\}$
引理4:所有后缀连接形成一棵树
显然。
引理5:后缀连接形成的树上,儿子节点的 endpos 被父亲的 endpos 包含,并且一个节点的儿子们没有交集。
首先,包含很容易想到,因为父亲的串是儿子的后缀,由引理 2 就知道了。
没有交集的话,假设一对兄弟由交集,那么它们必然由 endpos 集合的包含关系,也就是互为后缀(引理2)。假设$u , v$为$f$的儿子,$u$是$v $的后缀,我们知道它们的等价类不同,但是$u$是$f$的儿子,意味着它比$f$长那么根据 link 的定义$v$必然会连接向节点$u$,而不是节点$f$。
后缀连接形成的树叫做 parent tree 。而 parent tree 的实质是一棵前缀树。
同时我们有$v$开始往上沿着 link 跳,得到的所有 串 的的长度组成的集合是$[0,len[v]]$。
构造SAM
SAM的构造同样采用增量法。
我们假设根节点是$t_0$,添加一个字符$c$,我们添加一个点在最后,我们需要知道的是它的 link , son , 以及哪些点可以通过$c$转移到它。
我们记录添加前,整个串所代表的节点位置是$last$。
现在我们创建一个节点$cur$并且让$len(cur) = len(last) + 1$。这个时候我们并没有确定$link(cur)$。
考虑怎么确定其他点向这个点的 son 的关系。原来可以转移到$last$的某一个后缀的所有转移,都可以通过添加字符$c$转移到$cur$。我们开始从 last 向上跳,记录当前跳到的节点是$p$,如果$p$没有向$c$的转移,那么我们添加从$p$向$c$的转移到$cur$。一直跳直到当前节点拥有向$c$的转移或者到达了根。假如我们顺利跳到了根节点,就意味着字符串中没有出现过添加$c$后字符串的后缀,也就是没有出现过字符$c$(因为出现过$c$那么$c$就是字符串的后缀)。这种情况我们可以直接把$cur$ 的 link 指向根。
假设当前节点是$p$,它拥有向$c$的转移,假设$p$向$c$转移到了$q$,我们分两种情况来做:
-$len(q) = len(p) + 1$这种情况下,$q$上的最长的也只是$p$上最长的添加了一个$c$得到的后缀,我们就知道当前整个串的最长后缀就是$len(q)+1$。因为$p$是第一个跳到的节点,是第一个添加$c$后仍然存在于字符串中的节点。又$len(p) + 1 = len(q)$所以$q$就是$cur$连向的点。
否则,$len(q) > len(p) + 1$这种情况下,$q$上最长的必然不是$cur$的后缀。因为长度大于了$len(p)+1$而$len(p)+1$必然是当前整个串最长的后缀,我们可以把这个节点拆开,像这样:
上面这一段表示的点就是$link(cur)$指向的位置。它的 endpos 等价类又添加了一个新的位置(结尾),包含了下面这一些后缀表示的节点,显然就有下面这些后缀组成的点的 link 就是上面这一段。
同时我们需要继续从$p$跳 link 把原来指向$q$的点指向 cut 开的上面那一段组成的点。
别忘了更新$last$
代码丢在最后的。先来写另一个重要的东西:
SAM + 线段树合并
这是一种常用的套路今天学了一下。
有时我们需要一个 SAM 上一个节点的 endpos 集合。这个时候可以先把每个结束位置对应节点的 pos 设置为这个位置(用权值线段树存)。然后就 dfs 一下,合并线段树就好了。
那么这样复杂度为啥是对的呢,考虑一个点的所有儿子的 endpos 集合是没有交集的,因此总的线段树叶子个数是不会超过 2n 的,总空间复杂度是$O(nlogn)$
例题没意外应该会写的(🐦💨💨)。
写了。。在这里
- NOI 2018 你的名字
广义 SAM
如果要严谨的证明建议看 2015 年的论文。
大概就是对 Trie 来建 SAM 。
对于一个串,它的 endpos 集合是 Trie 上的所有点,满足对于这些点存在一个祖先,使得从祖先到这个点的路径连起来是这个串。和普通字符串上的 endpos 集合定义是很像的。
然后广义 SAM 上一个节点仍然表示一个 endpos 等价类,后缀链接连向的是最长的后缀使得它的 endpos 和这个点上的点不一样。其实是和 SAM 很类似的啦。
说一下广义SAM的构建:
最简单轻松的,如果只是插入一些字符串,那么插入一个后把 last 设置为根就好了。
据说这种构造在某些时候是会死掉的
但是好写啊,如果单词重复据说会加多点。。但是平时写由于很难出错一直写的这个。标准的,对于所有串构造一棵 Trie 然后 BFS 这个 Trie 来构造。加入一个点的时候就把 last 设置为它父亲加入完时的 last。
注意,写 DFS 是会被卡的!可以看这篇博客
广义SAM的复杂度和 SAM 类似,节点数也是 2n 的啦。。具体内容见论文。
1 |
|