后缀自动机学习笔记

看了一天,勉强知道了些后缀自动机的皮毛,趁着还清醒写一发自用。

自动机

看看这个名词,马上想到了AC自动机及回文自动机。自动机实际上就是给你一个模版串,然后你建出一个点与点之间的转移方式。让询问的字符串在上面按照你规定的方式转移,最后停下来的节点就代表某种匹配的结果。

后缀自动机

后缀自动机的目的是为了匹配所有后缀。显然只要是子串,都有可能在后续的匹配(即往后面加字符)中成为后缀。所以后缀自动机实质上保存了所有的子串信息。

复杂度

所有状态及转移是O(n)的,具体可见各大佬的分析。

怎么做

实现是后缀自动机的难点

直接保存子串是n*n的,显然不行,但注意到匹配后缀的信息有很多重复。如bcbca,已知我要输入一个后缀,输入c,可能接着输入的是bca或a。输入bc,可能接下来输入的也是bca或a,所以本质上bc和c的后续转移是一样的。意识到这个,其实n*n的子串信息有很多可以合并。

哪些可以合并呢

再次看bcbca这个串,发现一个子串可能匹配的后缀与它的出现位置有关。若它出现在[L,R]则接下来的字符可能是[R+1,N],也就是说,这个子串S出现后,接下来可能匹配的字符只与它出现的位置的右端点有关,那么,将它出现的所有位置的右端点记到一个集合里,称作Rg(S),如bcbca中,Rg('bc')= {2 , 4}。两个字符串的后续转移状态相同当且仅当Rg(S1)==Rg(S2)。既然后续转移相同,就可以在后缀自动机上共用一个节点,所以,后缀自动机上的一个节点实质上匹配了一类字符串,这些字符串转移到这个节点,当且仅当它们的Rg集合完全相同。

给出子串长度与字符串对应的Rg,很轻松就能确定这个子串。取任意1个ri,在前添字符即可。观察所有Rg相同的字符串,可以发现它们的长度为一个连续区间。可以看作在这些Rg集合中的ri点前添加字符,添加的字符较少时会有Rg集合之外的ri符合条件,添多了又会使集合中的ri不符合条件。如acabcaabc,Rg={5,9}的子串只有长度为2的bc与3的abc。长度为1时,第2个位置的c也能匹配,而长度为4时,cabc已经不等于aabc了。想象1个Rg所对应的字符串,不断删去它最前面那个字符,原来合法的ri现在也一定合法,删掉后还可能多出合法的ri来,把1个节点所对应的Rg集合进行这个操作得到的第1个新Rg所对应的节点记作他的fa。还是这个例子,Rg={5,9}时,最小的子串为bc,删去后得到c,Rg集合扩大为{2,5,9},所以Rg={5,9}的节点的fa就是Rg={2,5,9}的节点。把某个Rg可以对应的长度区间记作[min , max]

具体流程

显然不会保存它的Rg集合,但其fa有助于后缀自动机的构建。首先考虑一个节点u,若其有一条x的转移路径,表示在原来的Rg集合内,有一些ri后面是x,这条路径所通向的节点的Rg即为这些ri+1(把x加进去)。在一个串S的后面加入一个字符x,多出来的子串信息即为以x结尾的所有后缀,这些后缀可以由原来Rg中有最后一个位置L的节点转移过来。首先找到last,就是上一次添加时所存下来的,Rg={L}的节点(即表示1-L这个子串),所有Rg中有L的节点都是它的fa,把这些节点依次记作v1,v2......。新建一个节点np表示Rg={L+1}的串,那么对于没有x这条转移路径的节点来说,它们的所有Rg后都没有接过x,那么接上x后显然Rg集合就是{L+1}1个元素,全部朝np建边。一直找到1个有x路径的点vq为止。考虑它沿x路径所走到的点q,但小心,能走到q的点不一定原来Rg全部都有L这个元素。比较q的max与vq的max,若q的max等于vq的max+1,则q确实只能从vq及其fa后面加一个x转移过来,直接记np的fa为q即可,否则,需要建造1个只能从vq及其fa转移过来的节点nq,把nq记作np的fa,首先nq除了计入了末尾的x,没有任何与q的不同,所以转移路径与fa都与q相同,然后相较于q,其Rg集合中多了一个L+1,所以成为了q的fa。最后,要把vq以及vq的fa中 所有以q为 x的转移终点 的节点 的x的转移终点 都变成这个专门造出来的节点nq,就大功告成啦。

关于为什么新建nq,这里有一个例子:在abdabcdabcab后加c,第一个有c的Rg集合为{2 , 5 , 9 ,12 },即abdabcdabcab它的c的转移终点q为{6,10},即abdabcdabcab,但是{6,10}不仅可通过{2,5,9,12}加c转移,同样也可以通过{5,9}(abdabcdabcab)加c转移,这时若直接把L+1加入q的Rg集合,使q的Rg集合变为{6,10,13},则导致{5,9}本不能加c转移到{6,10,13},现在却能到达{6,10,13}。所以{5,9}c的转移终点只能是原来的{6,10},而我们只有新建一个节点表示{6,10,13}。


struct Node {
	int sn[26] , fa ; 
	int mx ; //存最大值用于判断q 
} p[200005] ; 

int las , lzw ; 
void getin ( char t ) 
{
	int np = ++lzw ; p[np].mx = p[las].mx + 1 ; 
	int nd = las ; las = np ; 
	while ( nd != -1 && p[nd].sn[t - 'a'] == 0 ) p[nd].sn[t - 'a'] = np , nd = p[nd].fa ; // 把没有x路径的节点赋值 
	if ( nd == -1 ) { p[np].fa = 0 ; return ; }  //没有节点有x路径,直接记fa为root,return 
	int q = p[nd].sn[t - 'a'] ; 
	if ( p[q].mx == p[nd].mx + 1 ) { p[np].fa = q ; return ; } // q可以直接用 
	int nq = ++lzw ;  // 新建节点nq 
	for ( int i = 0 ; i < 26 ; i++ ) p[nq].sn[i] = p[q].sn[i] ; 
	p[nq].fa = p[q].fa ; // 复制q的信息到nq 
	p[nq].mx = p[nd].mx + 1 ; 
	p[q].fa = p[np].fa = nq ;
	while ( nd != -1 && p[nd].sn[t - 'a'] == q ) p[nd].sn[t - 'a'] = nq , nd = p[nd].fa ; //把以q为终点的点终点赋为nq  
}

推荐陈立杰论文,挺详细的

------------------------------------------------------------------------后面为之后的补充

想要具体知道这个串是什么?你可以保留这个节点的任一个起点,记为d,那么这个节点可以对应的最长的串就是[d , d + mx - 1] 

转后缀树?反着建一遍后缀自动机,其fa树就是原串的后缀树,之后就为所欲为了。后缀自动机的fa树可以看做儿子的最长相同后缀就是父亲,而后缀树正好相反---儿子的最长相同前缀是父亲。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值