Palindromic Tree 回文自动机-回文树 例题+讲解

回文树,也叫回文自动机,是2014年被西伯利亚民族发明的,其功能如下:

1、求前缀字符串中的本质不同的回文串种类

2、求每个本质不同回文串的个数

3、以下标i为结尾的回文串个数/种类

4、每个本质不同回文串包含的本质不同回文串种类

(本文参考自Palindromic Tree——回文树【处理一类回文串问题的强力工具】,Palindromic Tree 回文自动机-回文树 解决回文串的神器)

下面介绍一些数组的意义

next[][]类似于字典树,指向当前字符串在两段同时加上一个字符

fail[] fail指针,类似于AC自动机,返回失配后与当前i结尾的最长回文串本质上不同的最长回文后缀

cnt[] 在最后统计后它可以表示形如以i为结尾的回文串中最长的那个串个数

num[] 表示以i结尾的回文串的种类数

len[] 表示以i为结尾的最长回文串长度

s[] 存放添加的字符

last 表示上一个添加的字符的位置

n 表示字符数组的第几位

p 表示树中节点的指针

 

在这里我想多说一下fail具体的意义,网上很多blog都说的不细致。

假设i的最长回文串为a,j的最长回文串为b,i>j,那么fail[i]=j当且仅当b为a的后缀且j是0~i-1中最长的。

 

以下内容转自http://blog.csdn.net/u013368721/article/details/42100363

一开始回文树有两个节点,0表示偶数长度串的根和1表示奇数长度串的根,且len[0] = 0,len[1] = -1,last = 0,S[0] = -1,n = 0,p = 2(添加了节点0、1)。

假设现在我们有串S = abbaabba。

首先我们添加第一个字符'a',S[++ n] = 'a',然后判断此时S[n - len[last] - 1]是否等于S[n],即上一个串-1的位置和新添加的位置是否相同,相同则说明构成回文。否则,last = fail[last]。此时last = 0,我们发现S[1 - 0 - 1] != S[1],所以last = fail[last] = 1,然后我们发现S[1 - (-1) - 1] == S[1](即自己等于自己,所以我们让len[1]等于-1可以让这一步更加方便)。

令cur等于此时的last(即cur = last = 1),判断此时next[cur]['a']是否已经有后继,如果next[cur]['a']没有后继,我们就进行如下的步骤:新建节点(节点数p++,且之后p = 3),并让now等于新节点的编号(now = 2),则len[now] = len[cur] + 2(每一个回文串的长度总是在其最长子回文串的基础上在两边加上两个相同的字符构成的,所以是+2,同时体现出我们让len[1] = -1的优势,一个字符自成一个奇回文串时回文串的长度为(-1) + 2 = 1)。然后我们让fail[now] = next[get_fail ( fail[cur] )]['a'],即得到fail[now](此时为fail[2] = 0),其中的get_fail函数就是让找到第一个使得S[n - len[last] - 1] == S[n]的last。然后next[cur]['a'] = now。

当上面步骤完成后我们让last = next[cur][c](不管next[cur]['a']是否有后继),然后cnt[last] ++。

此时回文树为下图状态:

现在我们添加第二个字符字符'b'到回文树中:

继续添加第三个字符'b'到回文树中:

继续添加第四个字符'a'到回文树中:

继续添加第五个字符'a'到回文树中:

继续添加第六个字符'b'到回文树中:

继续添加第七个字符'b'到回文树中:

继续添加第八个字符'a'到回文树中:

 

到此,串S已经完全插入到回文树中了,现在所有的数据如下:

然后我们将节点x在fail指针树中将自己的cnt累加给父亲,从叶子开始倒着加,最后就能得到串S中出现的每一个本质不同回文串的个数。

 

构造回文树需要的空间复杂度为O(N*字符集大小),时间复杂度为O(N*log(字符集大小)),这个时间复杂度比较神奇。如果空间需求太大,可以改成邻接表的形式存储,不过相应的要牺牲一些时间。

总的来说,这是一个很好的算法~

下面给出模板代码

 1 const int MAXN = 100005 ;
 2 const int N = 26 ;
 3 
 4 struct Palindromic_Tree {
 5     int next[MAXN][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成
 6     int fail[MAXN] ;//fail指针,失配后跳转到fail指针指向的节点
 7     int cnt[MAXN] ;
 8     int num[MAXN] ;
 9     int len[MAXN] ;//len[i]表示节点i表示的回文串的长度
10     int S[MAXN] ;//存放添加的字符
11     int last ;//指向上一个字符所在的节点,方便下一次add
12     int n ;//字符数组指针
13     int p ;//节点指针
14 
15     int newnode ( int l ) {//新建节点
16         for ( int i = 0 ; i < N ; ++ i ) next[p][i] = 0 ;
17         cnt[p] = 0 ;
18         num[p] = 0 ;
19         len[p] = l ;
20         return p ++ ;
21     }
22 
23     void init () {//初始化
24         p = 0 ;
25         newnode (  0 ) ;
26         newnode ( -1 ) ;
27         last = 0 ;
28         n = 0 ;
29         S[n] = -1 ;//开头放一个字符集中没有的字符,减少特判
30         fail[0] = 1 ;
31     }
32 
33     int get_fail ( int x ) {//和KMP一样,失配后找一个尽量最长的
34         while ( S[n - len[x] - 1] != S[n] ) x = fail[x] ;
35         return x ;
36     }
37 
38     void add ( int c ) {
39         c -= 'a' ;
40         S[++ n] = c ;
41         int cur = get_fail ( last ) ;//通过上一个回文串找这个回文串的匹配位置
42         if ( !next[cur][c] ) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
43             int now = newnode ( len[cur] + 2 ) ;//新建节点
44             fail[now] = next[get_fail ( fail[cur] )][c] ;//和AC自动机一样建立fail指针,以便失配后跳转
45             next[cur][c] = now ;
46             num[now] = num[fail[now]] + 1 ;
47         }
48         last = next[cur][c] ;
49         cnt[last] ++ ;
50     }
51 
52     void count () {
53         for ( int i = p - 1 ; i >= 0 ; -- i ) cnt[fail[i]] += cnt[i] ;
54         //父亲累加儿子的cnt,因为如果fail[v]=u,则u一定是v的子回文串!
55     }
56 } ;

 

posted @ 2018-01-10 21:40  大奕哥&VANE  阅读(4564)  评论(0编辑  收藏  举报