回文自动机
本文参考自原文链接
回文自动机,又叫回文树,是由俄罗斯人 MikhailRubinchik于2014年夏发明的(http://codeforces.com/blog/entry/13959).
这是一种比较新的数据结构,在原文中已有详细介绍与代码实现。
回文树其实不是严格的树形结构,因为它有是两棵树,分别是偶数长度的回文树和奇数长度的回文树,树中每个节点代表一个回文串。
为了方便,第一棵树的根是一个长度为0的串,第二棵就是为-1的串,不要感到奇怪,就是-1。
可以证明,最多只有n个结点(n是串的长度)。这个可以用Manacher算法来证明。
如果某结点代表的是串ccabacc,那么它的父亲代表的串就是去掉前后两个字符cabac。
每个点还有一个fail指针,表示这个串的后缀中最长的回文串,比如babab的fail指向bab,bab的指向b。
方法的思想和KMP,AC自动机很类似,如果你理解了KMP与AC自动机,那么这个算法基本可以一看就懂。
- 数据说明
- len[i]:节点i的回文串的长度
- next[i][c]:节点i的回文串在两边添加字符c以后变成的回文串的编号(和字典树的next指针类似)
- fail[i]:类似于AC自动机的fail指针,指向失配后需要跳转到的节点
- cnt[i]:节点i表示的回文串在S中出现的次数(建树时求出的不是完全的,count()加上子节点以后才是正确的)
- num[i]:以节点i回文串的末尾字符结尾的但不包含本条路径上的回文串的数目。(也就是fail指针路径的深度)
- last:指向最新添加的回文结点
- S[i]表示第i次添加的字符
- p表示添加的节点个数
注意:回文自动机里面的节点都表示的是一个回文串!!!而不是一个下标/字符!
先贴一波代码
#include <queue>
#include <cstdio>
#include <set>
#include <string>
#include <stack>
#include <cmath>
#include <climits>
#include <map>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#define LL long long
#define ULL unsigned long long
using namespace std;
const int MAXN = 100005 ;
const int N = 26 ;
char s[MAXN];
struct Palindromic_Tree
{
int next[MAXN][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成
int fail[MAXN] ;//fail指针,失配后跳转到fail指针指向的节点
int cnt[MAXN] ;
int num[MAXN] ; // 当前节点通过fail指针到达0节点或1节点的步数(fail指针的深度)
int len[MAXN] ;//len[i]表示节点i表示的回文串的长度
int S[MAXN] ;//存放添加的字符
int last ;//指向上一个字符所在的节点,方便下一次add
int n ;//字符数组指针
int p ;//节点指针
int newnode(int l) //新建节点
{
for(int i = 0 ; i < N ; ++ i) next[p][i] = 0 ;
cnt[p] = 0 ;
num[p] = 0 ;
len[p] = l ;
return p ++ ;
}
void init() //初始化
{
p = 0 ;
newnode(0) ;
newnode(-1) ;
last = 0 ;
n = 0 ;
S[n] = -1 ;//开头放一个字符集中没有的字符,减少特判
fail[0] = 1 ;
}
int get_fail(int x) //失配后,在回文串x中的所有后缀里找到一个串右端+s[n]依然构成回文串
{ //这里因为一定是从长的找到最短的,所以找到的一定是最长的
while(S[n - len[x] - 1] != S[n]) x = fail[x] ;//判断此时S[n-len[last]-1]是否等于S[n]
//即上一个串-1的位置和新添加的位置是否相同,相同则说明构成回文,否则,last=fail[last]。
return x ;
}
void add(int c,int pos) //cur,last,now都代表一个字符串,而不是一个下标/字符
{
printf("%d:",p);
c -= 'a';
S[++ n] = c ; //n代表字符下标
int cur = get_fail(last) ; //通过上一个回文串找这个回文串的匹配位置
printf("%d ",cur); //c+cur+c代表以c结尾的最长的回文串,cur对应原串中的位置就是以c前一个字符结尾的子串的位置
if(!next[cur][c]) //如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
{
int now = newnode(len[cur] + 2) ; //新建节点
fail[now] = next[get_fail(fail[cur])][c] ; //和AC自动机一样建立fail指针,以便失配后跳转
next[cur][c] = now ;
num[now] = num[fail[now]] + 1 ;
for(int i=pos-len[now]+1; i<=pos; ++i) printf("%c",s[i]);
} last = next[cur][c] ;
cnt[last] ++ ;
putchar(10);
}
void count()
{
for(int i = p - 1 ; i >= 0 ; -- i) cnt[fail[i]] += cnt[i] ;
//父亲累加儿子的cnt,因为如果fail[v]=u,则u一定是v的子回文串!
}
} run;
int main()
{
scanf("%s",&s);
int n=strlen(s);
run.init();
for(int i=0; i<n; i++) run.add(s[i],i);
run.count();
return 0;
}
看其他博客讲的,都看不懂,可能是自己太笨了...
所以就直接拿代码来啃,终于理解这个算法的原理。
回文自动机,其实关键是理解fail,基础就是我上面说的那句话————回文自动机的节点表示的是一个回文串!!!
然后难点就是fail[now] = next[get_fail(fail[cur])][c] ; 这句代码
首先定义后缀回文串:字符串的一个后缀(不包括字符串本身),该后缀是回文串
//因为这里now=c+cur+c已经代表了以c结尾的最长的回文串,那么如果这个匹配失败,我们
需要找到fail[now]=c+w+c——以c结尾的次长的回文串,该回文串的一个性质——
该回文串fail[now]是now的最长回文串后缀,因为这样我们才能保证在now匹配失败后,
fail[now]能立即被匹配,不需要任何移动。这一步就可以借助fail[cur]来完成
例如
bbabba
最后一个字符'a'的最长回文now是"abba",
cur="bb",fail[cur]="b",然后在fail[cur]中没有找到满足条件的解,
一直搜到x=0,表示长度为2的回文串,len[x]=1,发现还是不对,
然后x=fail[0]=1,长度为1的回文串,len[x]=-1,发现就是他本身
那么他的fail[now]="a"
abbaabba
最后一个字符'a'的now是"abbaabba",
cur="bbaabb",fail[cur]="bb"
他的fail[now]="abba"
然后next[get_fail(fail[cur])][c] 非空的原因见图
看完这个还不懂得可以去看一下贴在上面原来大牛的博客
最后贴一波他的功能
1.求串S前缀0~i内本质不同回文串的个数(两个串长度不同或者长度相同且至少有一个字符不同便是本质不同)
2.求串S内每一个本质不同回文串出现的次数
3.求串S内回文串的个数(其实就是1和2结合起来)
4.求以下标i结尾的回文串的个数
- len[i]:节点i的回文串的长度
- next[i][c]:节点i的回文串在两边添加字符c以后变成的回文串的编号(和字典树的next指针类似)
- fail[i]:类似于AC自动机的fail指针,指向失配后需要跳转到的节点
- cnt[i]:节点i表示的回文串在S中出现的次数(建树时求出的不是完全的,count()加上子节点以后才是正确的)
- num[i]:以节点i回文串的末尾字符结尾的但不包含本条路径上的回文串的数目。(也就是fail指针路径的深度)
- last:指向最新添加的回文结点
- S[i]表示第i次添加的字符
- p表示添加的节点个数
-