1. 程式人生 > >Palindromic Tree 迴文自動機-迴文樹 解決迴文串的神器

Palindromic Tree 迴文自動機-迴文樹 解決迴文串的神器

迴文樹,也叫回文自動機,是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(字符集大小)),這個時間複雜度比較神奇。如果空間需求太大,可以改成鄰接表的形式儲存,不過相應的要犧牲一些時間。

給出幾道練習題,程式碼也會在下一篇文章中給出。

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