回文利器——回文自动机

前(che)言(dan)

回文树,也叫回文自动机,是2014年被西伯利亚民族发明的(找不到百度百科,从一篇博客里蒯过来的node

做为解决回文问题的大杀器,回文自动机功能强大,实现技巧充满智慧。——dalao数组

一个性质

一个长度为N的字符串最多有N个不一样的回文子串。
为何?

咱们考虑加入一个字符能产生多少新的回文子串。

如今加入红圈位置的字符,假设绿框框起的都是回文串。而后咱们发现一个神奇的事情

由于最大的绿框框起的串是回文的,因此蓝框框起的串和最小绿框框起的串是相反的。
而后又由于最小的绿框框起的是回文串(也就是说跟它相反的串就是它自己),因此蓝框框起的串和绿框框起的块是相同的。
也就是说这个最小绿框框起的串已经出现过了。不能提供一个新的本质不一样回文子串。
因此,加入一个字符以后只有最长的一个回文串多是一个新的本质不一样的回文串。
因此能够证实:一个长度为N的字符串最多有N个不一样的回文子串。
这个性质比较重要。函数

简述

回文自动机是一类能够接受字符串的全部回文子串的自动机。
状态数 \(O(n)\)
转移函数 \(O(n)\)
能够在线 \(O(n)\) 构造
回文自动机的一个节点表明一个回文子串。
由于刚刚的性质,因此回文自动机的状态数 \(O(n)\)

上图就是字符串bacaa的回文自动机。(节点上的数字是这个点的编号)
发现回文自动机有两棵树。一棵树表明长度为偶数的回文串,一棵树表明长度为奇数的回文串。
咱们须要记录如下数值:
len[u] \(:\) \(u\) 节点表明回文串的长度。上图中在节点周围的黑色数字。
fa[u] \(:\) \(u\) 节点表明回文串的最长回文后缀表明的节点,上图中红色的边指向的就是本身的最长回文后缀。也就是说 \(fa[u]\) 其实就是上图的红边( \(0\) 点到 \(1\) 点的红边没有上述意义,只是为了方便实现)一直跳 \(fa\) 就等于遍历 \(u\) 节点的全部回文后缀。
tran[u][c] \(:\) 转移函数,自动机必备的东西也就是上图中的黑色边,表示在 \(u\) 表明的回文串的两端加上字符 \(c\) 以后的回文串。
num[u] \(:\) 上图中并无体现 \(num[u]\) ,表明 \(u\) 节点表明回文串的回文后缀个数。
L[i] \(:\) 并非回文自动机上的东西,表明原字符串以 \(i\) 结尾的回文后缀长度。
size[u] \(:\) \(u\) 点表明的回文串的数量。spa

用途

维护了上面的这些东西。回文自动机能够求下面的东西:
1.求前缀字符串中的本质不一样的回文串种类(就是节点数)
2.求每一个本质不一样回文串的个数(\(size\) 数组)
3.如下标i为结尾的回文串长度(\(L\)数组)3d

普通增量法构造

建议结合代码理解构造。
一开始只有两个点0,1。
因此code

void init(){
        len[0]=0;fa[0]=1;len[1]=-1;fa[1]=0;
        tot=1;last=0;
        memset(trans[1],0,sizeof(trans[1]));
        memset(trans[0],0,sizeof(trans[0]));
}

这个应该不用解释。(PS:代码中tot为节点数,last为上次插入操做后的最长回文后缀长度,后缀自动机也有这个东西)blog

int new_node(int x){
    int now=++tot;
    memset(trans[tot],0,sizeof(trans[tot]));
    len[now]=x;
    return now;
}
void ins(int c,int n){
    int u=last;
    while(s[n-len[u]-1]!=s[n])u=fa[u];
    if(trans[u][c]==0){
        int now=new_node(len[u]+2);
        int v=fa[u];
        while(s[n-len[v]-1]!=s[n])v=fa[v];
        fa[now]=trans[v][c];
        trans[u][c]=now;
        num[now]=num[fa[now]]+1;
    }
    last=trans[u][c];size[last]++;
    L[n]=len[last];
}

增量法的主体函数—— \(ins\)
传两个参, \(c\) 表明当前插入的字符, \(n\) 表明当前插入字符在原串中的下标。
第一个 \(while\) 循环其实是在寻找以当前位置为结尾的最长回文串。字符串

while(s[n-len[u]-1]!=s[n])u=fa[u];


绿框框起的是回文串插入蓝圈位置的字符,从大到小枚举回文后缀看红圈位置的字符是否和蓝圈位置的字符相等。
而后判断以当前位置为结尾的回文串是否已经出现过。博客

if(trans[u][c]==0)

若是出现过直接维护一些值结束。it

last=trans[u][c];size[last]++;
L[n]=len[last];

这应该能看得懂吧。。。
若是没有出现过,咱们新建一个节点就好了。
可是咱们还要维护一些量,好比新建节点的最长回文后缀。
因此咱们接着进行第一个 \(while\) 差很少的工做——寻找以当前位置为结尾的第二长回文串后缀。来做为新建节点的最长回文后缀。
找到以后再维护一些量就好了。

int now=new_node(len[u]+2);
int v=fa[u];
while(s[n-len[v]-1]!=s[n])v=fa[v];
fa[now]=trans[v][c];
trans[u][c]=now;
num[now]=num[fa[now]]+1;

仔细研究咱们发现,构造其实很精妙。
咱们若是一直找不到以当前位置为结尾的回文串,在一直跳 \(fa\) 的时候最终会到达 \(1\) 节点。
咱们看看 \(while\) 中的式子 \(s[n-len[u]-1]\)\(u=1\) 的时候是多少?
由于 \(1\) 点的长度为 \(-1\)\(len[1]=-1\)) ,发现 \(s[n-len[1]-1]\) 就是 \(s[n]\) 因此 \(while\) 最后必定会找到一个回文串也就是新加入的那一个字符。
假如第一个\(while\)找最长串就是这个字符了,你会发现你第二个 \(while\)找次长串 仍是会找到这个字符,由于\(fa[1]=0,fa[0]=1\)\(0\)\(1\) 节点其实是一个环。这样最后的维护仍是正确的。
下面是完整的模板

struct PAM{
    int len[N],fa[N],size[N],num[N],tot,last,trans[N][27];
    void init(){
        len[0]=0;fa[0]=1;len[1]=-1;fa[1]=0;
        tot=1;last=0;
        memset(trans[1],0,sizeof(trans[1]));
        memset(trans[0],0,sizeof(trans[0]));
    }
    int new_node(int x){
        int now=++tot;
        memset(trans[tot],0,sizeof(trans[tot]));
        len[now]=x;
        return now;
    }
    void ins(int c,int n){
        int u=last;
        while(s[n-len[u]-1]!=s[n])u=fa[u];
        if(trans[u][c]==0){
            int now=new_node(len[u]+2);
            int v=fa[u];
            while(s[n-len[v]-1]!=s[n])v=fa[v];
            fa[now]=trans[v][c];
            trans[u][c]=now;
            num[now]=num[fa[now]]+1;
        }
        last=trans[u][c];size[last]++;
        L[n]=len[last];
    }
}pam;
相关文章
相关标签/搜索