回文树(Palindrome Tree)/回文自动机(Palindrome Automaton)学习小记

简介

回文树(回文自动机),是解决一类回文串问题的强大数据结构,比 Manacher 扩展了很多功能。
这个数据结构比较新,由来自战斗民族的神犇MikhailRubinchik在2014年的Petrozavodsk夏令营提出。
这个数据结构代码量其实超级少。

必备技能

Manacher
最好会至少一种自动机

分析

回文树严格来讲是由两棵树构成的森林,再加上一堆后缀链(失配链)。其中一棵树代表长度为奇数的回文串,另一棵树代表长度为偶数的回文串。树上每个节点都代表一种与其它节点不同的回文串,所以每个点上都有诸如长度 len 和出现次数 cnt 之类的权值。
后缀链 fail 连向代表以这个点代表的回文串的最长回文后缀的点(有点长,自己断句)。
初始时,偶数树的根节点代表一个空串,长度为 0 ,后缀链连向奇数树的根节点。奇数树的根节点则比较神,代表一个被吞掉一个字符的串(是不是很鬼畜???),长度为 1 (后面会讲为什么要这样),后缀链连向它自己。
插入操作过程中,我们需要记录当前插入的串的最长回文后缀 suf 。设原串为 s ,当我们要加入第 i 个字符时,我们就从 suf 这个点开始,往后缀链一直跳,直到到达一个点 x 使得 s[i1len[x]]=s[i] ,也就是该点代表的回文后缀两边字符相等,可以变成一个新的回文串(注意:当我们跳到奇数树根时, s[i1len[x]]=s[i1(1)]=s[i] ,这样我们加入本质为单个字符的回文串了)。
我们找到 x 点后,先判断 x 是否存在两边字符为 s[i] 的儿子节点,如果有,就将儿子节点的 cnt 加一。否则新建节点, cnt 1 。那么新建点的后缀链怎么弄呢?我们可以对 fail[x] 做同样的过程(还是一直往 fail 跳,找合法节点),那么该点的 s[i] 儿子就是我们要找的 fail (当然存在找不到的情况,我们就将其 fail 设为空串即偶数树根,这个在实现时,我们可以将每个点不存在的儿子初始为偶数树根,这样就不用特判了)。

由于blog主特别懒,这里就不配图了,大家结合代码理解理解吧。

建完树,我们要重算一次每个回文串出现次数。过程很简单,就是倒序枚举节点,假设枚举到 x ,我们给 cnt[fail[x]] 加上 cnt[x] 即可。
然后剩下的就是结合题目乱搞了。

代码实现

注:以下代码对应题目[APOI2014]回文串,也是算法的出处,推荐大家去做一做。

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;

const int N=300005;
const int C=26;

long long ans;
char str[N];
int n;

struct Palindrome_Tree
{
    int next[N][C],cnt[N],fail[N],len[N];
    int tot,suf;

    int newnode()
    {
        for (int i=0;i<C;i++)
            next[tot][i]=0;
        cnt[tot]=0,fail[tot]=0;
        return tot++;
    }

    void init()
    {
        tot=0;
        int p;
        len[p=newnode()]=0;
        p=fail[p]=newnode();
        len[p]=-1;
        fail[p]=p;
        suf=0;
    }

    int getfail(int x,int l)
    {
        while (str[l-1-len[x]]!=str[l])
            x=fail[x];
        return x;
    }

    int insert(int x)
    {
        int c=str[x]-'a';
        int p=getfail(suf,x);
        if (!next[p][c])
        {
            int q=newnode();
            len[q]=len[p]+2;
            fail[q]=next[getfail(fail[p],x)][c];
            next[p][c]=q;
        }
        p=next[p][c];
        cnt[p]++;
        suf=p;
        return suf;
    }

    void calc()
    {
        for (int i=tot-1;i>=0;i--)
        {
            ans=max(ans,1ll*cnt[i]*len[i]);
            cnt[fail[i]]+=cnt[i];
        }
    }
}t;

int main()
{
    freopen("palindrome.in","r",stdin);
    freopen("palindrome.out","w",stdout);
    scanf("%s",str);
    n=strlen(str);
    t.init();
    for (int i=0;i<n;i++)
        t.insert(i);
    t.calc();
    printf("%lld\n",ans);
    fclose(stdin);
    fclose(stdout);
}

复杂度

首先有一条定理

长度为n的字符串本质不同的回文子串数目最多为n。

证明有很多,可以通过 Manacher 算法过程证明,也可以分析字符串性质,在这里不再讨论,参考资料的最后一项讲了。
然后空间复杂度显然为 O(n|Σ|) Σ 为字符集)或 O(n) ,取决于你怎么存树。
时间复杂度我是这样分析的,我们考虑后缀链的长度,显然最多为 n ,每次插入的过程就相当于给后缀链长度减去若干之后加上 1 (包括计算 fail 的过程在内),长度最多加 n 次,显然我们最多也只能减 n 次。所以时间复杂度为 O(n)

参考资料

国外博客:http://adilet.org/blog/25-09-14/
Codeforces上的介绍:http://codeforces.com/blog/entry/13959
一个不懒的人(然而他配的图也是copy原论文的)的博客:http://blog.csdn.net/u013368721/article/details/42100363
PDF的论文(不是原论文,作者Victor Wonder,写的很详细,UOJ上有链接):PalindromicTree.pdf

你可能感兴趣的:(数据结构,字符串,OI,自动机,回文串)