迴文利器——迴文自動機

前(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;
相關文章
相關標籤/搜索