咱们可以先从字面意思来理解什么是回文树,回文树 (回文自动机) 实际上是奇偶两棵树,每一个节点代表一个本质不同的回文子串(一棵树上的串长度全部是奇数,另一棵全部是偶数),原串中每一个本质不同的回文子串都在树上出现一次且仅一次。

一个节点的 fail 指针指向它的最长回文后缀(不包括自身,所有空 fail 均连向 1)。归纳容易证明,当在原串末尾新增一个字符时,回文树上至多会新增一个节点,这也证明了一个串本质不同的回文子串个数不会超过 n。

建树时采用增量构造法,当考虑新字符 s [i] 时,先找到以 s [i-1] 为结尾的节点 p,并不断跳 fail。若代表新增回文子串的节点已存在则直接结束,否则通过 fail [p] 不断跳 fail 找到新节点的 fail。

0,1 号节点均不代表串,常数大于 manacher。初始化 fail [0]=fail [1]=1,len [1]=-1,tot=1,last=0。


概述

回文自动机(简称PAM,又称回文树,Palindromic Tree)是一种用于处理回文串的结构,在其结构内可以找到原串中的所有回文子串,顾名思义,回文树是一个用来解决回文串相关问题的数据结构。


结构

就像线段树、平衡树等其它树结构一样,回文树由若干个节点组成,每个节点代表一个回文串(palindrome)。

如图为串abba的PAM示意图,其中,实线表示回文串的扩展转移边,虚线表示后缀链接。

回文树结构


节点信息

每个节点对应一个唯一的回文子串。

(1)回文串出现的次数cnt(u)

(2)回文串的长度len(u)

(3)可以不保存具体的字符串信息

为了方便,我们规定,偶回文树树根len(rt0)=0,奇回文树树根len(rt1)=−1。


转移边

转移边u→v对节点的意义是,将u对应的回文串左右两边加上同一个字符c得到节点v对应的回文串。


失配边

指向该节点对应子串的最长回文后缀

为了方便我们规定偶回文树树根rt0指向奇回文数根rt1


节点信息转移

记对应回文串出现的次数cnt(u),则 $$cnt(u)=\sum _{v=son(u)} {cnt(v)}$$

记对应回文串的长度len(u),若节点v是由节点u转移而来的,则有len(v)=len(u)+2


构造

和后缀自动机类似,用增量法构造,每次在母串后插入一个字符,更新PAM的复杂度是O(1)的,因此构造自动机是O(n)的。

我们记录上一次插入节点的位置last,对于新插入的字符c,我们检查之前的最长回文后缀的前一个字母是否和c相同,如果相同,那么就创建一个新的节点,否则我们沿着适配边找最长回文后缀的最长回文后缀(稍微有点绕口) 如当前的母串是 $$caba$$ 现在我要插入新的字符 b。

那么我先看到最长回文后缀aba的前一个字母是c,和新插入的字符a不同(因此cabab不能组成回文子串),因此我沿着失配边找到aba的最长回文后缀a,由于前一个字符b与新插入的字符相同,能组成新回文子串aba,因此新建节点。

稍微注意的是s[0]赋值为一个特殊字符。

模板如下:

#include <bits/stdc++.h>#define rep(i, a, b) for (int i=a; i<=b; i++)#define drep(i, a, b) for (int i=a; i>=b; i--)#define inf 1e9using namespace std;typedef long long ll;const int maxn=300010;char s[maxn];int n;struct Ptree {
    int last;
    struct Node {
        int cnt, lenn, fail, son[27];
        Node(int lenn, int fail):lenn(lenn), fail(fail), cnt(0){
            memset(son, 0, sizeof(son));
        };
    };
    vector<Node> st;
    inline int newnode(int lenn, int fail=0) {
        st.emplace_back(lenn, fail);
        return st.size()-1;
    }
    inline int getfail(int x, int n) {
        while (s[n-st[x].lenn-1] != s[n]) x=st[x].fail;
        return x;
    }
    inline void extend(int c, int i) {
        int cur=getfail(last, i);
        if (!st[cur].son[c]) {
            int nw=newnode(st[cur].lenn+2, st[getfail(st[cur].fail, i)].son[c]);
            st[cur].son[c]=nw;
        }
        st[ last=st[cur].son[c] ].cnt++;
    }
    void init() {
        scanf("%s", s+1);
        n=strlen(s+1);
        s[0]=0;
        newnode(0, 1), newnode(-1);
        last=0;
        rep(i, 1, n) extend(s[i]-'a', i);
    }
    ll count() {
        drep(i, st.size()-1, 0) st[st[i].fail].cnt+=st[i].cnt;
        ll ans=0;
        rep(i, 2, st.size()-1) ans=max(ans, 1LL*st[i].lenn*st[i].cnt);
        return ans;
    }}T;int main() {
    T.init();
    printf("%lld\n", T.count());}


点赞(0)

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

Dotcpp在线编译      (登录可减少运行等待时间)