回文自动机是一个用来维护一个字符串的回文子串的工具
可以应用于求本质不同回文子串个数,回文子串出现次数,最小回文划分等问题
前置知识
m
a
n
a
c
h
e
r
\tt manacher
manacher
t
r
i
e
\tt trie
trie树
算法用途
维护一个字符串的回文子串
算法复杂度
时间
在末尾添加字符: O ( n ) O(n) O(n)
空间
O ( n × log ( ∣ S ∣ ) ) O(n\times \log(|S|)) O(n×log(∣S∣))
算法实现
结构
回文自动机由两颗树构成,分别用来存储长度为奇数的回文子串和长度为偶数的回文子串。
跟
t
r
i
e
trie
trie树 一样,回文自动机的每条边都表示一个字符,每个节点表示一个回文串(
t
r
i
e
trie
trie树可以理解为每个节点表示一个字符串)。
一般回文自动机每个节点都需要储存一个值,
s
i
z
i
siz_i
sizi,代表此节点的回文串的大小。
假设回文自动机有一个节点
u
u
u,另一个节点
v
v
v 为
u
u
u 的儿子,
u
u
u 到
v
v
v 的边的字符为
c
c
c,
s
t
r
i
str_i
stri 代表节点
i
i
i 表示的字符串。
那么有
s
t
r
v
=
c
+
s
t
r
u
+
c
str_v = c + str_u + c
strv=c+stru+c,
s
i
z
v
=
s
i
z
u
+
2
siz_v=siz_u+2
sizv=sizu+2。所以我们将储存奇回文的树的根节点的
s
i
z
siz
siz 设为
−
1
-1
−1 ,偶回文的根节点
s
i
z
siz
siz 设为
0
0
0。
则
a
b
b
a
a
abbaa
abbaa 的回文自动机(没有
f
a
i
l
fail
fail指针)如下图所示:
fail 指针
我们设
f
a
i
l
u
fail_u
failu 代表节点
u
u
u 的
f
a
i
l
fail
fail 指针指向的节点。
我们有
s
t
r
f
a
i
l
u
str_{fail_u}
strfailu 为
s
t
r
u
str_u
stru 的后缀,且不存在
s
t
r
v
str_v
strv,使得
∣
s
t
r
v
∣
>
∣
s
t
r
f
a
i
l
u
∣
|str_v| > |str_{fail_u}|
∣strv∣>∣strfailu∣,满足
s
t
r
v
str_v
strv 也为
s
t
r
u
str_u
stru 后缀。(没有回文后缀的回文串的
f
a
i
l
fail
fail 指向其所在的树的根节点,
s
i
z
siz
siz 为
0
0
0 的根节点的
f
a
i
l
fail
fail 指向
s
i
z
siz
siz 为
−
1
-1
−1 的根节点,
s
i
z
siz
siz 为
−
1
-1
−1 的根节点没有
f
a
i
l
fail
fail 指针)
构建
此操作将一个字符插入到原字符串的末尾。(
S
→
S
+
c
S\to S+c
S→S+c)
于是我们只需要对要构建的字符串
S
S
S 执行
∣
S
∣
|S|
∣S∣ 次此操作即可构建出
S
S
S 的回文自动机。
插入一个字符 c c c 时,我们枚举插入前的字符串的每个回文后缀(包括长度为 0 0 0 或 − 1 -1 −1 的),然后判断插入 c c c 之后,这个后缀是否还是一个回文串(只需判断此回文串前面的字符是否与插入的字符相同即可)。
如果是,则这个回文串就是插入 c c c 后字符串的一个回文后缀。我们就在表示这个回文后缀的节点下增加一个节点,表示新产生的这个回文串。如果表示这个回文后缀的节点已有回文串,那么就代表它又出现了一次。
如何快速枚举插入前的字符串的每个回文后缀?我们可以记录一个节点
l
a
s
t
last
last,节点表示的回文串为插入前的最长回文后缀。
因为
s
t
r
f
a
i
l
u
str_{fail_u}
strfailu 为
s
t
r
u
str_u
stru 的最长后缀,所以如果我们从
l
a
s
t
last
last 开始,一直跳
f
a
i
l
fail
fail,我们走过的每个回文串都是插入前字符串的回文后缀,且遍历顺序为从长到短。
因为遍历顺序为从长到短,我们也可以顺便更新新增节点的 f a i l fail fail指针,和更改 l a s t last last。我们只用把 l a s t last last 更改为第一个通过判断的回文串,每个新增节点的 f a i l fail fail 指向下一个新增的节点即可(如果这个新增节点为最后一个新增节点,我们把它的 f a i l fail fail 指向其所在的树的根节点)。