Palindromic Tree 回文自动机-回文树 解决回文串的神器

来源:互联网 发布:乙肝流行病学调查数据 编辑:程序博客网 时间:2024/05/03 08:55

回文树,也叫回文自动机,是2014年夏天战斗民族发明的,其功能如下:

1、求前缀字符串中的本质不同的回文串种类

2、求每个本质不同回文串的个数

3、以下标i为结尾的回文串个数/种类

4、每个本质不同回文串包含的本质不同回文串种类

(本文参考自Palindromic Tree——回文树【处理一类回文串问题的强力工具】)


在回文树中,每一个节点代表一个本质不同的回文串,因为长度为n的字符串最多有本质不同的回文串n个,故回文树中至多有n节点,再加上两个初始节点,至多有n+2个节点。

(以下代码和理解也是学习的鸟神的Palindromic Tree——回文树【处理一类回文串问题的强力工具】,自己修改后的理解

首先我们定义一些变量。
1.len[i]表示编号为i的节点表示的回文串的长度
2.next[i][c]表示编号为i的节点表示的回文串在两边添加字符c以后变成的回文串的编号(和Trie类似)。
3.fail[i]表示节点i失配以后跳转不等于自身的节点i表示的回文串的最长后缀回文串(类似后缀自动机的parent指针)。
4.cnt[i]表示节点i表示的本质不同的串的个数(建树时求出的不是完全的,最后count()函数按照拓扑序跑一遍以后才是正确的)
5.num[i]表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数,也可以用来在建树过程中统计下标j的为结尾的回文串个数。
6.last指向新添加一个字母后所形成的最长回文串表示的节点。
7.S[i]表示第i次添加的字符(一开始设S[0] = -1(可以是任意一个在串S中不会出现的字符))。
8.p表示添加的节点个数。
9.n表示添加的字符个数。

那么我们在建树过程中,每添加一个字符c,至多会增加一个新的种类的回文串(或是之前已经有的回文串),由于形成的最长的回文串一定是以字符c结尾,那么这个回文串必定是cTc形式,其中T为回文串。想到这里就可以使用last的fail指针可以递归找到一个最长的回文后缀且前一个字符为c,否则此回文串为c,可以看代码理解。

回文树最重要的是建树过程,在建树时num[last]即是以此回文串的结尾为尾端的回文串种类,也是以当前下标为结尾的回文串的个数。

最后延拓扑序(类似后缀自动机,不过这里的拓扑序就是是从p-1到0)更新cnt

下面给出代码:

const int maxn = 100005;// n(空间复杂度o(n*ALP)),实际开n即可const int ALP = 26;struct PAM{ // 每个节点代表一个回文串    int next[maxn][ALP]; // next指针,参照Trie树    int fail[maxn]; // fail失配后缀链接    int cnt[maxn]; // 此回文串出现个数    int num[maxn];    int len[maxn]; // 回文串长度    int s[maxn]; // 存放添加的字符    int last; //指向上一个字符所在的节点,方便下一次add    int n; // 已添加字符个数    int p; // 节点个数    int newnode(int w){ // 初始化节点,w=长度        for(int i=0;i<ALP;i++)            next[p][i] = 0;        cnt[p] = 0;        num[p] = 0;        len[p] = w;        return p++;    }    void init(){        p = 0;        newnode(0);        newnode(-1);        last = 0;        n = 0;        s[n] = -1; // 开头放一个字符集中没有的字符,减少特判        fail[0] = 1;    }    int get_fail(int x){ // 和KMP一样,失配后找一个尽量最长的        while(s[n-len[x]-1] != s[n]) x = fail[x];        return x;    }    void add(int c){        c -= 'a';        s[++n] = c;        int cur = get_fail(last);        if(!next[cur][c]){            int now = newnode(len[cur]+2);            fail[now] = next[get_fail(fail[cur])][c];            next[cur][c] = now;            num[now] = num[fail[now]] + 1;        }        last = next[cur][c];        cnt[last]++;    }    void count(){        // 最后统计一遍每个节点出现个数        // 父亲累加儿子的cnt,类似SAM中parent树        // 满足parent拓扑关系        for(int i=p-1;i>=0;i--)            cnt[fail[i]] += cnt[i];    }}pam;

构造回文树需要的空间复杂度为O(N*字符集大小),时间复杂度为O(N*log(字符集大小)),这个时间复杂度比较神奇。如果空间需求太大,可以改成邻接表的形式存储,不过相应的要牺牲一些时间。

邻接表用vector实现,代码可以参考TsinsenA1393. Palisection的代码。


给出几道练习题,代码也会在下一篇文章中给出。

1.ural1960. Palindromes and Super Abilities
2.TsinsenA1280. 最长双回文串
3.TsinsenA1255. 拉拉队排练
4.TsinsenA1393. Palisection
5.2014-2015 ACM-ICPC, Asia Xian Regional Contest G The Problem to Slow Down You
6.bzoj 3676


0 0
原创粉丝点击