模板 - 字符串 - 回文自动机

我好像有点明白了,字符串题目很多都是关注这个fail父亲,也就是当前节点代表的字符串的满足某种条件的最长真后缀。

对于回文自动机来说,下面的字段的意义是:

\(s\) :已经插入回文自动机的字符串,一般来说就是问题给的字符串的一个前缀。
节点:表示一个回文串,这个回文串在 \(s\) 中出现过,这个回文串的一半为这个节点所在的回文树的树根不断走ch到达这个节点时途径的边依次连起来,注意有奇数长度和偶数长度的区分。每个节点都表示一种本质不同的回文串,反之每种本质不同的回文串也是只对应一个节点。
\(ch\) :节点的信息之一,表示一个这个节点表示的回文串 \(p\) ,向两边加上ch对应的字符后,会得到一个新的回文串 \(p'\) ,假如这个新的回文串 \(p'\)\(s\) 中出现过,则ch会指向它。
\(fail\) :节点的信息之一,表示一个这个节点的fail父亲,fail指向一个更短的回文串 \(p'\) ,且这个 \(p'\) 是这个节点表示的回文串 \(p\)最长回文后缀 。与后缀自动机不同,显然一个节点在生成之后,其代表的信息就确定了,他所有的可能的回文后缀就已经确定的,不会像后缀自动机一样修改已有的fail父亲。
\(len\) :节点的信息之一,表示一个这个节点表示的回文串 \(p\) 的长度,类似于这个节点在回文树中的深度,只不过是每次+2,且奇数回文树的树根的len是-1的。
\(tot\) :约等于回文自动机中的节点的总数,取决于你怎么看到奇偶回文树中本身有的树根节点。并不是每次Extend都会新建节点。
\(top\)\(s\) 的长度,每次Extend会增加。
\(last\)\(s\) 的最后一个字符所在的最长回文串的节点。每次Extend结束都会改变(就算与原来相等也是一种改变)last。相应的可以在这里统计一些别的信息,比如这个节点曾经被作为last多少次,易知这个节点表示的是一种回文串,当过多少次last就说明有多少次新增节点之后曾经当过包含最后一个字符的最长回文串。假如要计算这个回文串真正出现了多少次(而不是作为包含最后一个字符的最长回文串多少次),需要从这个节点的fail孩子们中继承(因为fail孩子们对应的回文串是包含这个节点对应的回文串的)。

struct PAM {
    char s[MAXN];
    int ch[MAXN][26];
    int fail[MAXN];
    int len[MAXN];
    int tot, top, last;

    void Init() {
        len[0] = 0;
        fail[0] = 1;
        len[1] = -1;
        fail[1] = 0;
        tot = 1;
        top = 0;
        last = 0;
        memset(ch[0], 0, sizeof(ch[0]));
        memset(ch[1], 0, sizeof(ch[1]));
    }

    int NewNode(int l) {
        int now = ++tot;
        memset(ch[tot], 0, sizeof(ch[tot]));
        len[now] = l;
        return now;
    }

    void Extend(char c) {
        s[++top] = c;
        c -= 'a';
        int u = last;
        while(s[top - len[u] - 1] != s[top])
            u = fail[u];
        if(ch[u][c] == 0) {
            int now = NewNode(len[u] + 2);
            int v = fail[u];
            while(s[top - len[v] - 1] != s[top])
                v = fail[v];
            fail[now] = ch[v][c];
            ch[u][c] = now;
        }
        last = ch[u][c];
    }
} pam;

字符集为26,数据量为1e6的PAM有28倍4字节的1e6,加上一些零头差不多要128MB。

其实pam包含的ch转移数也是一倍n,因为每次最多新连接一次ch[u][c],所以假如想要节省字符集使用的空间,可以改成使用链式前向星,这样就需要遍历这个节点的ch包含的链式前向星,单次转移的复杂度增加到了字符集的大小,一种折中的方案是把ch分成若干块,例如4块,每个块存7种转移,也就是['a','g']存在第一组链式前向星ch[0]中,验证['a','g']中某个转移是否存在复杂度为字符集的大小/4,空间消耗减少为8倍(每组前向星同样至多有n个转移,每个前向星消耗data和nxt两倍空间)。这样看来倒不如只用一个链式前向星,毕竟1e6的数据量就算字符集是52也不太影响。

posted @ 2020-03-21 13:23  KisekiPurin2019  阅读(141)  评论(0编辑  收藏  举报