AC快樂機——最最通俗易懂的AC自動機講解!

AC快樂機

衆所周知,KMP是算法競賽中常用的字符串匹配算法,該算法通過對模式串構建next數組的方式,十分有效的提高了匹配的效率。

單一模式串的匹配可以構造next,那如果模式串有多個,也同樣能通過構造next的方式匹配嗎?

小企鵝

Fail指針

給你多個模式串,也就是給你一棵Trie,在Trie上進行匹配。假設我們有能力構建出一棵Trie的next,考慮KMP中的next的定義(指向最長的後綴),那麼一棵Trie的next是長這樣的(以模式串he,she,him,hers,shit爲例):

這是個Trie

這棵Trie的next是這樣的(沒畫的都指向根):

它的fail

現在要拿主串ashers上去匹配,匹配的方式和KMP一樣:如果可以接着向下走則向下走,否則爬next

那麼匹配的路徑爲root-root-s-h-e-e-r-s,最終匹配成功

匹配的複雜度顯然是O(n+min(n,m))O(n+min(n,m))的(其中nn爲主串長度,mm爲最長的模式串長度)

如果在Trie樹上能構造出next的話,匹配複雜度就會變成線性,這就是AC自動機基礎的原理啦,AC自動機中next的名字叫做Fail指針(也稱後綴節點),接下來的所有next都會用Fail代替

Fail指針是從一個Trie節點(對應字符串α\alpha)到另一個Trie節點(對應的字符串ω\omegaα\alpha的最長後綴)的有向邊

匹配時,如果我們到達了不能繼續匹配的節點,我們沿着Fail指針爬以維持儘可能更大的前綴

對於Trie樹上每一個節點,我們都可以構造一個Fail指針

Fail指針的構造?

Fail指針的些許小性質:

1.根的所有兒子的Fail都是根

2.節點x的Fail指向y(非根),那麼x的t兒子的Fail指向y的t兒子的Fail

以上圖爲例,sheh的Fail指向的是he中的h,那麼she兒子she的Fail就會指he兒子he

3.一個節點一直沿着Fail爬會爬到根,期間深度是不斷減小的

這樣我們有了一個構建Fail的大體思路,對於每一個點,對他所有的兒子構建Fail,然後bfs下去,對t兒子構建Fail的過程可以通過不斷爬Fail找到第一個有t兒子的點這一途徑得到

但也會有一些不美妙的情況使這個過程變得很不美觀

一個栗子

假設上圖苟利國家生死以有一個兒子,用剛纔所說的方法構造的Fail指針就需要沿着藍色箭頭爬好遠好遠好遠好遠好遠好遠才能找到,這個過程會讓過程及複雜度極其不優美,AC自動機用Trie圖簡化了這個過程

Trie圖

(爲了方便,將根所有不存在的兒子都設爲根)

對於Trie樹上的每一個節點xx,它的α\alpha邊指向的是它下一個字符tt,表示如果當前狀態是xx,待匹配字符是α\alpha,那麼匹配到的點是tt

設字符集爲SS,點xx可匹配的(向外延伸出去的)字符集SSS'\subseteq S,即αS\forall \alpha \in S',都可以xx(不通過爬Fail的方式)由向外延伸(到pp)

此過程描述爲:當xx的下一個字符是α\alpha(αS\alpha \in S')時,走到的節點是pp

但是如果當前狀態是xx,下一個字符βS\beta \notin S',也就代表着在此失配,那麼根據剛纔所描述的方法,AC自動機需要爬Fail找到第一個有β\beta兒子的點yy繼續匹配(到qq)。

此過程可以描述爲:當xx的下一個字符是β\beta(βS\beta \notin S')時,走到的節點是qq

這兩個過程描述起來十分相似,那麼我們爲什麼不將qq直接看作xxβ\beta兒子呢?

對於節點xx,如果它並沒有β\beta兒子,那麼沿着找到第一個有β\beta兒子的節點yy,並把yyβ\beta兒子看作是自己的β\beta兒子

我們把這個操作稱爲NTR,NTR過後的圖叫Trie圖

上圖理論上的Trie圖是長這樣的

Trie圖

但實際上是這樣的

真實Trie圖

如果xxNTR了yyα\alphayyNTR了zzα\alpha,可以直接看作xxNTR了zzα\alpha,也就是說如果我們按照Fail的順序構建,NTR操作只要考慮一個Fail就可以了

在Trie圖上的匹配只要無腦向後走就可以了,也就是說,構建完Trie圖,Fail就沒什麼用了

Trie圖本質上是個有向圖

Fail指針 Trie圖的構造!

struct Node{
    Node *ch[26],*fail;
    bool b;
    Node():fail(NULL){
        b=false;
        for(int i=0;i<26;i++)
        	ch[i]=NULL;
    }
}*root=new Node;
queue<Node*>q;
inline void Insert(char *s){///構建Trie
    Node *x=root;
    int len=strlen(s+1);
    for(int i=1;i<=len;i++){
        if(!x->ch[s[i]-'0']) x->ch[s[i]-'0']=new Node;
        x=x->ch[s[i]-'0'];
    }
    x->b=true;
}
inline void GetFail(){///AC自動機可以以bfs的方式構建
    root->fail=root;///根的Fail是他自己
    for(int i=0;i<26;i++){
        if(root->ch[i]) q.push(root->ch[i]),root->ch[i]->fail=root;
        ///將根兒子的Fail指向根,並放入隊列裏
        else root->ch[i]=root;
        ///如果root沒有i這個兒子,就把他的兒子賦爲自己,這樣以上的性質2對根也滿足了
    }
    while(!q.empty()){
        Node *x=q.front();q.pop();
        for(int i=0;i<26;i++){
            if(x->ch[i]) x->ch[i]->fail=x->fail->ch[i],q.push(x->ch[i]);
            ///根據性質2搞出各個兒子的Fail
            else x->ch[i]=x->fail->ch[i];///NTR操作(
        }
        Node *tmp=x->fail;
        while(tmp!=root && !tmp->b) tmp=tmp->fail;
        if(tmp->b) x->b=true;
        ///如果它的某一後綴是一個模式串,那麼它也肯定包含模式串(一些特定的題統計時會用到)
        
    }
}

在Trie圖上的匹配過程可以直接當成Trie樹上匹配,代碼也是一樣的

inline int Match(char *c){
    int len=strlen(c+1);
    int ans=0;
    Node *x=root;
    for(int i=1;i<=len;i++){
        int k=c[i]-'a';
        x=x->ch[k];///一直往下走就行了
        Node *tmp=x;///這部分是統計的...
        while(tmp!=root && !tmp->b){
            ans+=tmp->num,tmp->b=true;
            tmp=tmp->nex;
        }
    }
    return ans;
}

Fail樹

把所有Fail指針當作一條無向邊,可以發現構成的是一個樹形結構

小例題:

給定一個n個單詞的文章,求每個單詞在文章中的出現次數 (n200n\le200,單詞總長106\le 10^6)

小企鵝

大體思路:先將所有的串建一個AC自動機,構建的Fail時候到一個點就將所有它的所有Fail都加上1

顯然會T

可以發現,一個點一定是它Fail樹中子樹點的後綴(如果串s能匹配到這裏,s就會是子樹所有點的後綴)

那麼一個串的出現次數事實上就是它Fail樹子樹中單詞數量的和

然後沒了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章