前(fei)言(hua):
咳咳,既然是学习笔记,那肯定是边学边写的啊,所以会持续更新呀。
SAM是啥子玩意?(在大佬讲之前完全听都没听过,但是根据PPT的排布以及讲的是字符串主题来看,这应该是“后缀自动机”了(本蒟蒻听过后缀自动机,仅仅只是听过,但是SAM这个名字就没听过了))
然后我百度了一下SAM,搜出来这玩意......
噫!好!我人傻了!当场爆粗口来了句氧化钙。
然后我耐心(气急败坏)的查了查“后缀自动机”|,终于正常了。
正题:
后缀自动机 (suffix automaton, SAM) 是一个能解决许多字符串相关问题的有力的数据结构(”高级算法“)。
后缀自动机中几个重要的概念:
1.endpos(S)表示的是子串S在原串中出现的位置(末位置),例如在ababa中,endpos(ab)={2,4}(好像也有称为right集合的,不管了),这玩意是个等价集
2.如果存在子串T以及S使得endpos(T)=endpos(S),那么我们则将S以及T归为同一类中,endpos相等的子串归在一起称为一个"状态",
我们给各个"状态"依次编号,那么就会得到若干个状态,在这里我们约定我们记为Str(x)表示状态x的所有字符串(x是状态的编号)
同样拿上面的例子来说事,endpos(ab)={2,4},endpos(b)={2,4},这样我们将"ab"以及"b"归为一类,称为一个"状态"
3.后缀链接,这玩意下文详细讲
4.parent树,这玩意下文详细讲
后缀自动机的几个性质
先看一组endpos的数据:
当原串为:“abbabaa”的时候
状态1. 空串 endpos={1,2,3,4,5,6,7}
状态2. a endpos={1,4,6,7}
状态3. ab endpos={2,5}
状态4. b endpos={2,3,5}
状态5.abb,bb endpos={3}
状态6.abba,bba endpos={4}
状态7.ba endpos={4,6}
状态8.abbab,bbab,bab endpos={5}
状态9.abbaba,bbaba,baba,aba endpos={6}
状态10.abbabaa,bbabaa,babaa,abaa,baa,aa endpos={7}
话说,泥萌有没有发现什么规律。
对于每一个状态,里面每一个串的长度都是逐一递减的,而且后一个串就是前一个串去掉第一个字符后得到的后缀
那么性质一来了:
如果endpos(S)=endpos(T),那么S为T的后缀或者T为S的后缀
不妨令|S|<|T|,那么S一定是T的后缀
证明:如果S不是T的后缀,那么S的第1到len(S)的字符中必然有一位与对应的T的第len(T)-len(S)+1到len(T)位不同
因为endpos(S)=endpos(T),显然与以上假设矛盾,那么不可能有S的第1到len(S)的字符中必然有一位与对应的T的第len(T)-len(S)+1到len(T)位不同
所以如果endpos(S)=endpos(T),那么S为T的后缀或者T为S的后缀.
性质二:
观察得到,如果S为T的后缀,则endpos(S)包含集合endpos(T)
如果S不是T的后缀,则endpos(S)∩endpos(T)=∅
性质三(转移函数)**:
挺重要的。
对于一个状态X,我们在属于它的串后面加上一个字符ch,只要这个字符的是属于由 endpos(状态X中的字符串)后一个的字符组成的集合nxt,
那么就满足产生的新串都属于同一个状态(这里可能我表述不大清楚,多给几个例子,方便理解)
例如上面的例子:
对于开篇的串的状态6,endpos()={4},那么nxt集合等于{"b"}
那么定然abba+b,bba+b属于一个同一个状态
对于串“abdabe”中
某个状态为: ab,b endpos={2,5}
那么ab+d与b+d一定是属于同一类,
ab+e 与 b+e一定是同一类。
对于串"acbabacba"中
某个状态为:acba,cba endpos={4,9}
那么对acba+b 与 cba+b一定是同一类的
那么我们将会得到一个函数:f(S,c)=str(S)+(c∈nxt),S为状态号,nxt可以通过endpos求得,也就是s[endpos(str(S))+1]所得到的字符集
这个函数将会用来进行匹配以及构建SAM!!!!
性质四:
前面说了“对于每一个状态,里面每一个串的长度都是逐一递减的,而且后一个串就是前一个串去掉第一个字符后得到的后缀”,那么我们观察得到每个状态都是“不完全的”
所谓的不完全就是无法直接从状态中的最长串一直延续到空串
例如“状态9.abbaba,bbaba,baba,aba endpos={6}”
如果它是完全的,那么就会有接下来的:
“状态9.abbaba,bbaba,baba,aba,ba,a endpos={6}”,
显然a与ba的endpos不与前面几项endpos相同对不对?所以它们不是同一个状态
我们要是强行将状态9完善呢?那么就会需要将“ba”以及“a”接进来,那么就需要一条链,这条链为:
状态9----->状态7------>状态2----->状态1
这条链肯定是唯一性的(不会有一模一样的两条链),并且被指向的那个状态所包含的字符串一定是当前状态 所有*(一定是的,因为串中字符串的长度是严格递减的) 字符串的后缀。
同时这些链它们的终点一定会空集,也就是状态1,所以,把状态1看成根节点的话,若干条这样的链也就构成了一棵树,也就是大名鼎鼎的parent树,我们惊奇的发现我们要的后缀自动机的节点就是parent树的节点!
bb了这么多(我太菜了,因为是边理解边写的,可能会有些误差,希望大佬们能够指出),现在终于到了讲构造后缀自动机的时候了(激动的小手准备开始打板子了)!
还是先讲一下构造的思路(只贴代码不讲思路的博客都是耍流氓):
后缀自动机的构造是在线的,我们通过每次插入节点的方式来实现(有之前手写二叉堆内味了)
- 令 last 为添加字符 c 之前,整个字符串对应的状态(一开始我们设 last=0 ,算法的最后一步更新 last )。
- 创建一个新的状态 cur,并将 len(cur) 赋值为 len(last)+1 ,在这时 link(cur) 的值还未知。
- 现在我们按以下流程进行(从状态 last 开始)。如果还没有到字符 c的转移,我们就添加一个到状态 cur 的转移,遍历后缀链接。如果在某个点已经存在到字符 c 的转移,我们就停下来,并将这个状态标记为p 。
- 如果没有找到这样的状态 p ,我们就到达了虚拟状态 −1 ,我们将 link(cur) 赋值为 0并退出。
- 假设现在我们找到了一个状态 p ,其可以通过字符 c 转移。我们将转移到的状态标记为 qqq 。
- 现在我们分类讨论两种状态,要么 len(p)+1=len(q) ,要么不是。
- 如果 len(p)+1=len(q),我们只要将 link(cur) 赋值为 q 并退出。
- 否则就会有些复杂。需要 复制 状态 q :我们创建一个新的状态 clone ,复制 q的除了 len 的值以外的所有信息(后缀链接和转移)。我们将len(clone) 赋值为 len(p)+1 。
复制之后,我们将后缀链接从 cur 指向clone ,也从 q 指向 clone 。
最终我们需要使用后缀链接从状态 p 往回走,只要存在一条通过 ppp 到状态 q 的转移,就将该转移重定向到状态 clone 。 - 以上三种情况,在完成这个过程之后,我们将 last 的值更新为状态cur 。
以上带点内容借鉴(抄袭)于:[ HatsuneMiku神佬的题解](https://www.luogu.com.cn/problem/solution/P3804)
代码就先咕咕了。。。。。
鸣谢:sh妹,Rothen,涛队,万总帮助小蒟蒻MYCui