回文树

  对于文本T,设T’是T的逆序文本,若T'与T相同,那么称T为回文。好比aba、abba都是回文。node

  回文树是用于组织和统计文本T中全部回文的数据结构,能够优雅地解决大量回文有关的问题。如同AC自动机,后缀自动机等处理文本的数据结构同样,回文树的创建也拥有着线性的时间复杂度,而且其创建过程是在线的。算法

  下面咱们来描述回文树的定义和创建过程。数据结构

定义

  在文本T上创建的回文树中的结点表示文本T的某段子回文串,且文本T中的每一个子回文串都对应回文树中的一个结点,咱们将结点n所表明的回文记做n.repr。结点与其孩子之间经过轨迹联系,若某个结点u经过字符c标记的轨迹抵达到结点v(即存在c标记的边(u,v)),那么v所表明的回文等同于u所表明的回文的两端加上c,即v.repr = c + u.repr + c。函数

  对于每一个结点,咱们记录结点对应回文的长度。对于结点n,咱们记n.len为其回文长度,即n.len=|n.repr|。spa

  如同AC自动机和后缀自动机同样,回文树结点也有失败指针fail。对于结点x,设y=x.fail,那么y.repr是x.repr的后缀,且是最长后缀(|y.len|<|x.len|)。对于从某个结点x出发,沿着fail链能访问到的结点序列,咱们称为x的fail链。显然全部x的回文后缀都出如今了x的fail链上(根据定义)。指针

  回文树中初始有两个结点,even和odd。其中even.len = 0,即even表示的是空串(按照定义空串固然也是回文)。而odd.len=-1,这里可能会感受比较奇怪,可是看到后面就知道用处了。咱们顺便将even.fail设为odd,而odd.fail设为空NIL便可。实际上咱们考虑到孩子的存在,而孩子是在回文两端加上相同的字符,即对于父亲father和孩子child,必定有father.len+2=child.len,那么even的孩子是偶数长度的回文串,而odd的孩子均为奇数长度的回文串,将even.len设为0,能够保证T中全部偶数串均可以挂在even或even的后代结点上,而将odd设为-1,能够帮助全部奇数长度的回文挂在其下。而且因为空串even的存在,咱们能保证每一个非空回文的fail指针均能指向一个结点(空串是全部串的后缀,且是回文,所以知足fail指针的要求)。code

  回文树中还须要有一个指针last,初始时其指向even和odd都可。last会在每次向树中加入新的字符后,指向当前读取的文本(T的某段前缀)的能造成最长回文的后缀(因为单个字符也是文本,所以last始终能指向一个有效结点)。blog

创建

  好的,上面就是回文树中的定义,下面咱们要讲述回文树的创建过程。假设咱们已经利用文本T的前缀T[1...k-1]创建了一株合法的回文树(此时全部的上面的定义都对于文本T[1...k-1]是知足的)。那么如今读入新的字符c=T[k]。ast

  如今咱们须要作的工做是将在T[1...k-1]创建的回文树Tree[k-1]转变为在T[1...k]上创建的回文树Tree[k]。先观察两株回文树有什么区别,区别在于Tree[k]可能拥有比Tree[k-1]更多的回文,而这个回文必定是以新加入的字符T[k]做为结尾的。咱们考虑须要加入多少个结点,等同于考虑T[1...k]较T[1...k-1]多了哪些回文。设T[l...k]是T[1...k]全部回文后缀中最长的回文,那么咱们能够保证T[1...k]只可能比T[1...k-1]多了回文T[l...k](固然也可能两者拥有相同的回文子串)。那么对于r>l,若T[r...k]也是回文,咱们如何保证T[1...k-1]中包含回文T[r...k]呢?这源于回文的性质,因为T[r...k]是T[l...k]的后缀,且两者都是回文,所以T[l...(l+k-r)]=T[r...k],而(l+k-r)<k,所以T[r...k]是T[1...k-1]的某个子串。class

  好了,根据上一段的说明,咱们了解到最多只须要向Tree[k-1]中加入一个结点就可使得与Tree[k]有相同的结点。固然也有可能T[l...k]已经存在于T[1...k-1],这时候咱们就不须要追加结点。不管哪一种状况,咱们都须要先找到T[l...k]在Tree[k-1]中的父结点。T[l...k]的父结点必然是T[l+1...k-1](若是l=k,那么表明的就是odd)。而咱们注意到last记录了T[1...k-1]中的最长后缀回文,而T[l+1...k-1]也是T[1...k-1]的某个回文后缀,所以T[l+1...k-1]必然出如今了last的fail链上。而根据T[l...k]是T[1...k]的最长回文后缀,所以T[l+1...k-1]必然是last的fail链上最长的某个符合下面条件的结点x:T[k-x.len-1]=T[k]。因为fail链上的结点的长度是递减的,所以T[l+1...k-1]是last的fail链上首个知足该条件的结点。写做代码就以下:

x = last;
for(; k - x.len >= 0; x = x.fail);
for(; T[k - x.len - 1] != T[k]; x = x.fail);

  咱们已经成功找到了父亲,以后利用父亲是否存在c标记的轨迹,就能够判断T[l...k]是否已经存在于Tree[k-1]中了,若是已经存在天然就不须要增长了。可是根据last的定义,咱们须要将last调整为x.next[c]才能保证上面的定义不被破坏,即last指向文本的最长回文后缀。

if(x.next[c] != NIL)
    last = x
    return

  固然还有一种是T[l...k]不存在于Tree[k-1]。咱们须要加入新的结点now,来保证能建立出Tree[k]。很显然now.len = x.len + 2。而且咱们还须要创建now与x的父子联系:

now = new-node
now.len = x.len + 2
x.next[c] = now

  可是作完了这些就OK了吗,看看咱们新建立的Tree[k]还违反了上面提出的哪些定义。是的,now的fail指针尚未正确设置。因为咱们说过结点的fail指针指向的是该结点的最长回文后缀,而因为now和now.fail均为回文,所以now.fail也是now的回文前缀,即在now加入以前,now.fail已经存在于原来的回文树中了,这也说明了now.fail永远能指向正确的结点,而且不会由于后面新的字符的加入而改变。咱们接下来聊一聊如何找到now.fail。

  因为now=c+x+c,而now.fail是now的后缀,所以now.fail在剔除两端的c以后获得的结点y(即y是now.fail的父亲),必然是x的后缀回文(注意因为x没有c标记的轨迹,所以x不多是y,故y只多是x的后缀回文)。而x的后缀回文均落在x的fail链上,因此咱们能够在fail链上找到y,而y也就是x的fail链上最长(换言之首个)知足下面条件的结点:T[k-y.len-1]=T[k]。固然这个过程当中,若x是odd,那么now实际上只有一个字符c,咱们上面所说的寻找fail指针指向的y.next[c]的算法找到的fail长度至少为1,所以没法找到x的fail,咱们能够特判,并将其fail指针设置为空串,即even。

if(x.len == 1)
    now.fail = even
else
    y = x.fail
    for(; T[k-y.len-1] != T[k]; y = y.fail)
    now.fail = y.next[c]

  固然也不要忘记须要将last设置为正确值now。

last = now

  将上面几部分代码合并起来,咱们就获得了从Tree[k-1]到Tree[k]的转移函数。

分析

  时间复杂度的分析以下:

  按照算法,每次读入一个新的结点,咱们最多将循环结束后的last的len增长2,同时每次沿着fail链寻找新结点的父亲x的时候,每次循环都将会使得last的len减小1。在下一次读入新字符后,咱们先找到x(last的某个后缀),以后必然会从x.fail开始沿着其fail链移动寻找y,而x.fail的len是必然要不可能大于last.fail.len的。所以咱们发现now.fail.len<=last.fail.len+2。每次从x.fail开始沿着其fail链移动寻找y,都会使得last.fail.len减小至少1,而每读入一个字符最多使得last.fail.len增长2,由last.fail.len>=0能够推出最多沿着从x.fail开始沿着其fail链移动寻找y共计2|T|次。其它每次读入一个字符的时间复杂度均为常数O(1),所以时间复杂度为O(|T|)+O(|T|)+|T|*O(1)=O(|T|)。
  空间复杂度的分析以下:

  每次读入一个字符最多建立1个结点,加上初始时建立的even和odd,总计最多建立|T|+2个结点,所以空间复杂度为O(|T|)。同时这也说明一段文本T中最多有|T|种不一样的回文。

相关文章
相关标签/搜索