前言
回文树(EER Tree,Palindromic Tree,也被称为回文自动机)是一种可以存储一个串中所有回文子串的高效数据结构。使用回文树可以简单高效地解决一系列涉及回文串的问题。
结构
回文树是由两个树组成。根节点分别是
0
0
0 和
−
1
-1
−1,分别表示长度为偶数和奇数的回文串(存储方式和 trie 类似)。一棵表示 abba 的回文树长这样:

其中,黑色边表示 fail 指针。回文串的 fail 指针,指向「后缀最长回文串」。
转移
现在给定字符串 s s s,假设我们已经处理好了 [ 1 , p − 1 ] [1, p-1] [1,p−1] 之前的回文树,现在要添加字符 s p s_p sp。
假设存在 [ 1 , p − 1 ] [1, p-1] [1,p−1] 的后缀回文串长度为 l e n len len,当且仅当 s p − l e n − 1 = s p s_{p - len - 1} = s_p sp−len−1=sp 时,可以添加新的回文串。
我们可以不断地查询 fail 指针,假设查询到长度为
l
e
n
len
len 的回文串,那么直到
s
p
−
l
e
n
−
1
=
s
p
s_{p - len - 1} = s_p
sp−len−1=sp 时,添加新的回文串
[
p
−
l
e
n
−
1
,
p
]
[p - len - 1, p]
[p−len−1,p]。
现在我们要更新
[
p
−
l
e
n
−
1
,
p
]
[p - len - 1, p]
[p−len−1,p] 的 fail 指针。我们回到刚刚长度为
l
e
n
len
len 的回文串,继续查询 fail 指针,直到继续存在
s
p
−
l
e
n
−
1
=
s
p
s_{p - len - 1} = s_p
sp−len−1=sp 时,更新 fail 指针。

找到 A 所代表的节点,然后在下面增加一个
s
p
s_p
sp,并且将
s
p
s_p
sp 节点的 fail 指针指向 B。
Oi-wiki 上面有详细的证明证明时间复杂度是正确的 O ( n ) O(n) O(n)。
实现
首先,我们要实现 getfail 函数,表示从
[
1
,
p
−
1
]
[1, p-1]
[1,p−1] 到 A,和 A 到 B 的操作。
int getfail(int x, int i){
// 表示节点编号为 x 中,满足第 s[i] = s[i - len - 1] 的最长子串
while(i - len[x] - 1 <= 0 || s[i - len[x] - 1] != s[i]) x = fail[x];
return x;
}
假如我们从第
i
(
p
)
i(p)
i(p) 号位置插入新字符
c
(
s
p
)
c(s_p)
c(sp),有 insert 函数。
int insert(char c, int i){// 在第 i 位新增一个字符 c
int p = getfail(now, i);// 此时 p 是自动机中 A 的编号
if(!t[p][c - 'a']){// 新的子节点
fail[++tot] = t[getfail(fail[p], i)][c - 'a'];
t[p][c - 'a'] = tot;
len[tot] = len[p] + 2;
// cnt[tot] = cnt[fail[tot]] + 1;
}
now = t[p][c - 'a'];
return cnt[now];
}
例题 P5496 【模板】回文自动机(PAM) - 洛谷
代码如下,增加了 cnt 数组。
#include <bits/stdc++.h>
#define pii pair<int, int>
#define pb push_back
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
using ll = long long;
const int N = 5e5 + 5;
string s;
int len[N], fail[N], t[N][26];
// 分别表示节点编号为 i 的长度、最长后缀回文串编号,
int cnt[N], n, tot = 1, now;
int getfail(int x, int i){
// 表示节点编号为 x 中,满足第 s[i] = s[i - len - 1] 的最长子串
while(i - len[x] - 1 <= 0 || s[i - len[x] - 1] != s[i]) x = fail[x];
return x;
}
int insert(char c, int i){// 在第 i 位新增一个字符 c
int p = getfail(now, i);// 此时 p 是笔记中 A 的编号
if(!t[p][c - 'a']){// 新的儿子
fail[++tot] = t[getfail(fail[p], i)][c - 'a'];
t[p][c - 'a'] = tot;
len[tot] = len[p] + 2;
cnt[tot] = cnt[fail[tot]] + 1;
}
now = t[p][c - 'a'];
return cnt[now];
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>s;
n = s.size();
s = " " + s;
fail[0] = 1, len[1] = -1;
int lst = 0;
for(int i = 1;i <= n;i++){
if(i-1) s[i] = (s[i] + lst - 97) % 26 + 97;
lst = insert(s[i], i);
cout<<lst<<' ';
}
return 0;
}
2001

被折叠的 条评论
为什么被折叠?



