https://oi-wiki.org/string/pam/ 回文树讲解
len[i]:节点i的回文串的长度
next[i][c]:节点i的回文串在两边添加字符c以后变成的回文串的编号(和字典树的next指针类似)
fail[i]:类似于AC自动机的fail指针,指向失配后需要跳转到的节点(即为i的最长回文后缀且不为i)
cnt[i]:节点i表示的回文串在S中出现的次数(建树时求出的不是完全的,count()加上子节点以后才是正确的)
num[i]:以节点i回文串的末尾字符结尾的但不包含本条路径上的回文串的数目。(也就是fail指针路径的深度)
last:指向以字符串中上一个位置结尾的回文串
cur: 指向由next构成的树中当前回文串的父亲节点(即当前回文串是cur左右两边各拓展一个字符得来)
S[i]:第i次添加的字符
p:添加的节点个数
n:添加的字符个数
举个例子,比如当前串为cbbabb,那么最长的回文子串为bbabb,我们要加入字符a,那么a先与bbabb左边的字符(c)比较,发现不一样,于是跳转到bbabb的最长的后缀回文子串,也就是bb,继续匹配。发现bb左边的字符为a,和我们要加入的字符一样,所以新加入的len就是len(bb)+2=4,也就是子串abba。
分析
假设现在我们有串S=’abbaabba’。
先建两个树根,偶数长度的根为0,奇数长度的根为1。注意,我们将len[0]设为0,但将len[1]设为-1。将p、n、last均初始化为0。将S[0]设为-1,这是放一个字符集中没有的字符,减少特判。然后,我们将fail[0]指向1。
举个例子,若有串S=’abbaabba’。
首先我们添加第一个字符’a’,S[++ n] = ‘a’,然后将cur赋为get_fail(last)。其中的get_fail函数就是让找到第一个使得S[n-len[last]-1]==S[n]的last。注意,此处的n不为get_fail中的参数,依然为添加的字符个数。这样做的话,我们就可以通过fail构成的失配链找到last的所有回文后缀(包括它自己),然后从长到短依次判断此后缀的前一位是否等于S[n],等于则表明可以构成一个回文串。
判断此时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(fail[cur])是为了求出在cur的所有回文后缀中(不包括它自己,因为和AC自动机一样,fail[now]不能指向now),满足前一位等于S[n]的后缀,我们即可用它来往两边拓展一格,即为now的最长回文后缀(不包括它自己)。然后next[cur][‘a’] = now。
当上面步骤完成后我们让last = next[cur][‘a’](不管next[cur][‘a’]是否有后继),然后cnt[last] ++。
如上述方法插完所有字符后,我们将节点x在fail指针树中将自己的cnt累加给父亲,从叶子开始倒着加,最后就能得到串S中出现的每一个本质不同(两个串长度不同或者长度相同且至少有一个字符不同便是本质不同可以使用burnside引理)回文串的个数。
构造回文树需要的空间复杂度为O(N∗字符集大小),时间复杂度为O(N∗log(字符集大小),这个时间复杂度比较神奇。如果空间需求太大,可以改成邻接表的形式存储,不过相应的要牺牲一些时间。
功能
求串S前缀0~i内本质不同回文串的个数
求串S内每一个本质不同回文串出现的次数
求串S内回文串的个数(其实就是1和2结合起来)
求以下标i结尾的回文串的个数
例题
【JZOJ3654】【APIO2014】回文串(palindrome)
Problem
考虑一个只包含小写拉丁字母的符串s。我们定义s的一个子串t的“出现值”为 t在s中的出现次数乘以t的长度。 请你求出s的所有回文子串中的最大出现值。
Hint
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; #define N 300010//绝对要多开一点,因为回文树会多开两个节点,我就被这坑过一次 #define ll long long #define fo(i,a,b) for(i=a;i<=b;i++) #define fd(i,a,b) for(i=a;i>=b;i--) int i,nn; ll ans; char s[N]; struct Palindrome_Automaton//回文自动机 { int i,len[N],next[N][26],fail[N],cnt[N],last,cur,S[N],p,n; int newnode(int l)//新建节点 { fo(i,0,25)next[p][i]=0;//新建的节点为p,先消除它的子节点 cnt[p]=0; len[p]=l; return p++;//勿打成++p,因为此节点为p,我们应返回p } inline void init()//初始化 { p=n=last=0; newnode(0); newnode(-1); S[0]=-1; fail[0]=1; } int get_fail(int x) { while(S[n-len[x]-1]!=S[n])x=fail[x]; return x; } inline void add(int c,int pos)//插字符 { c-='a'; S[++n]=c; int cur=get_fail(last); if(!next[cur][c]) { int now=newnode(len[cur]+2); fail[now]=next[get_fail(fail[cur])][c]; next[cur][c]=now; } last=next[cur][c]; cnt[last]++; } void count()//统计本质相同的回文串的出现次数 { fd(i,p-1,0)//逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙 cnt[fail[i]]+=cnt[i]; } }run; int main() { freopen("palindrome.in","r",stdin); freopen("palindrome.out","w",stdout); scanf("%s",&s); run.init(); nn=strlen(s)-1;//千万要先把这个记录下来,因为求长度的时间复杂度是O(n)——直接扫一遍,碰到结束符才停止,我一开始把它直接塞进下方循环的nn里,就T了一遍 fo(i,0,nn)run.add(s[i],i); run.count(); fo(i,2,run.p-1)ans=max(ans,(ll)run.len[i]*run.cnt[i]); printf("%lld",ans); }