回文树 回文自动机 PAM 入门学习详解
回文树 回文自动机 PAM
是什么
- 顾名思义,回文自动机是一种自动机,其中的每个状态表示的是一个回文串,可以用来解决与回文串有关的许多问题。
- 和其他自动机一样,它也有转移边(树边)和失配指针(fail)。其中转移边表示的是在原回文串的两边各加一个字符,得到长度加 2 2 2的新回文串;fail指针则指向该回文串的最长回文后缀。
- 和其他自动机有所不同,它有两个根节点, 分别代表长度为偶数的串和长度为奇数的串。它们的长度分别为 0 0 0和 − 1 -1 −1(注意不是 1 1 1,为了添加 2 2 2的长度可以得到长为 1 1 1的回文串),以下分别称为奇根和偶根。
- 值得注意的是,偶根的fail指针指向的是奇根,而奇根的fail并不需在意,它的儿子中总会有长为 1 1 1的回文串,因而不可能会失配。
- 它大概是这样:
如何构造
- 首先初始化两个根节点。
a[0].len = 0, a[1].len = -1;
a[0].fail = 1;
la = 0;//上一个字符的最长回文后缀
- 已经构造好了前 i − 1 i-1 i−1个字符的回文树,当前需要加入第 i i i个字符 c c c。
- 从上一个字符的最长回文后缀开始,不断往fail链走,直到能够满足 c = s t i − l e n − 1 c=st_{i-len-1} c=sti−len−1,即直到某个串的左边的字符和它右边的字符 c c c相同。
- 那么这个点的 c c c儿子即为当前的最长回文后缀,注意需要判断 c c c儿子是否存在,如果不存在需要添加。
int x = la;
while(st[i] != st[i - 1 - a[x].len]) x = a[x].fail;
if(!a[x].p[st[i] - 'a']) a[x].p[st[i] - 'a'] = ++tot;//如果没有儿子则添加
la = a[x].p[st[i] - 'a'];
a[la].len = a[x].len + 2;
- 如果新建了一个点,则需要找到这个点的fail指针指向哪里。
- 类似AC自动机地,从它的父亲的fail开始不断往fail链跳,但是不能像AC自动机一样判断当前点是否有 c c c这个儿子,而是判断当前回文串的前一个字符是否为 c c c。
- 同时注意,当新建的点是长为 1 1 1的回文串时,fail指针要赋为偶根。(这个点的父亲是奇根,而此时我们不需要往它父亲的fail指针走,这就是为什么开始时提到不需要在意奇根的fail)
if(a[la].len == 1) x = 0;
else {
x = a[x].fail;
while(st[i] != st[i - 1 - a[x].len]) x = a[x].fail;
//错误写法:while(!a[x].p[st[i] - 'a']) x = a[x].fail;
x = a[x].p[st[i] - 'a'];
}
a[la].dp = a[x].dp + 1;
a[la].fail = x;
ls = a[la].dp;
- 过程示意图如下:
一些思考
思考一
- 前面提到的“不能”,是为什么呢?
- 在AC自动机上,fail指针指向能匹配上的最长后缀,在父亲跳fail时,只要跳到的点能有 c c c儿子,则与当前串必然相同;
- 而在回文树上,fail指针指向的必须是当前回文串的最长回文后缀,尽管跳到的点有 c c c儿子,但不能保证关于中心对称的另一边一定有字符 c c c,所以不一定满足。
- 简单举个例子:
- AC自动机中,求abbba串的fail指针,从abbb开始跳,若跳到bb时有a儿子,则必然会有bba串,于是可以把fail指针连给它;
- 回文树中,求abbba串的fail指针,从bbb开始跳,若跳到bb时有a儿子,但发现abbba串中并没有abba这样的回文串,所以单单这样判断是不行的。
思考二
- 正确的求法中,并没有判断是否有存在这样的一个儿子,那万一它不存在呢?
- 由于这是一个回文串,把它的fail指针位置关于中心对称,必然存在一个相同的串,所以它必然已经出现在回文树中了。
模板题
哈哈哈哈哈哈哈哈哈哈