淺談AC自動機

AC自動機

前(che)言(dan)

Aho-Corasick automation,該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法之一。一個常見的例子就是給出 \(n\) 個單詞,再給出一段包含 \(m\) 個字符的文章,讓你找出有多少個單詞在文章裏出現過。要搞懂AC自動機,先得有模式樹(字典樹)\(Trie\)\(KMP\) 模式匹配算法的基礎知識。html

Trie

這裏的Trie可不是什麼權值線段樹
就是常說的字典樹。
這個東西彷佛很簡單?不會的出門左轉問度娘
一個性質是兩個串的\(LCP\)就是這兩個串對應結束位置的\(LCA\)ios

簡述

(此圖蒯自網絡)(右上方的幾個字符串是模式串)
這就是一個\(AC\)自動機。
想要知道自動機是什麼的大佬請問度娘
它大致上就是一個 \(Tire\) 樹(Trie樹中字符應該在邊上,但這裏字符在點上不引響解釋),不過多了一些奇怪的東西,也就是圖中的虛箭頭。
這叫作fail指針\(Trie\) 樹中一個點的 \(fail\) 指針指向這個點表明字符串最長後綴對應的節點\(fail\) 指針體現了 \(KMP\) 的思想
能夠結合上面的圖理解。算法

性質

咱們發現若是咱們沿着一個點的 \(fail\) 邊一直跳,就能夠遍歷這個點表明字符串的全部後綴。用AC自動機解決的經典問題:多模式串匹配,就使用了AC自動機的這個性質。網絡

咱們發現 \(fail\)構成了一棵樹(以後叫作fail 樹),其中一個點的子樹中全部的點表明的串都包含這個點表明的串(或者說這個點表明的串這個點的 fail 樹上子樹中的點表明的串的子串)。 51nod 麥克打電話這個題就運用了AC自動機的這個性質。優化

構造

接下來說講如何構造\(AC\)自動機。
\(AC\)自動機就是\(Trie\)樹上多了一些\(fail\)邊,固然要先建出\(Trie\)樹。spa

void insert(char *s){
    int l=strlen(s+1);
    int now=0;
    for(int i=1;i<=l;i++){
        if(ac[now].nxt[s[i]-'a']==0)ac[now].nxt[s[i]-'a']=++tot;
        now=ac[now].nxt[s[i]-'a'];
    }
    ac[now].cnt++;
}

(PS:代碼中ac[u].nxt[i]表明 \(Trie\) 樹中 \(u\) 節點沿 \(i\) 字符這條邊走到達的節點,ac[u].cnt表明 \(Trie\) 樹中以 \(u\) 爲結束爲止的字符串數量)
而後就是如何建 \(fail\) 邊了。
考慮按長度遞增的順序(例如 \(bfs\) 序)依次求每一個節點的\(fail\)。假設節點 \(x\) 的父邊上的字符是 \(c\),那麼咱們就從\(fail[fa[x]]\) 開始沿着 \(fail\) 鏈往上跳,直到跳到一個節點 \(y\) 使得 \(y\) 有字符爲 \(c\) 的出邊,那麼這條出邊走到的兒子就是 \(fail[x]\)指針

void get_fail(){
    queue<int> q;
    for(int i=0;i<=25;i++)
        if(ac[0].nxt[i])q.push(ac[0].nxt[i]);
    while(!q.empty()){
    int u=q.front();
        q.pop();
        for(int i=0;i<=25;i++)
        if(ac[u].nxt[i]){
            q.push(ac[u].nxt[i]);
        int now=ac[u].fail;
        while(now&&ac[now].nxt[i]==0)now=ac[now].fail;
        if(now==0&&ac[now].nxt[i]==0)ac[ac[u].nxt[i]].fail=0;
        else ac[ac[u].nxt[i]].fail=ac[now].nxt[i];
        }
    }
}

(PS:代碼中ac[u].fail表明 \(u\) 點的 \(fail\) 邊指向節點。code

構造的複雜度分析

考慮對於屬於同個串的節點建 \(fail\) 的總複雜度,\(KMP\) 用的分析依然適用,所以時間複雜度爲總串長。htm

模板

以這道模板題爲例
把文本串在 \(Trie\) 上進行匹配,新加一個字符時,若當前節點沒有這個字符的出邊,就一直沿着 \(fail\) 往上跳,直到跳到一個有該字符的出邊爲止,而後走到出邊指向的兒子。而後把節點記錄的結束位置個數統計一下就行了。blog

查詢時間複雜度爲文本串長度。複雜度分析同 \(KMP\) 複雜度分析

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
const int N=1001000;
int n,ans,tot;
char s[N];
struct AC{
    int nxt[26],cnt,fail;
}ac[N];
void insert(char *s){
    int l=strlen(s+1);
    int now=0;
    for(int i=1;i<=l;i++){
        if(ac[now].nxt[s[i]-'a']==0)ac[now].nxt[s[i]-'a']=++tot;
        now=ac[now].nxt[s[i]-'a'];
    }
    ac[now].cnt++;
}
void get_fail(){
    queue<int> q;
    for(int i=0;i<=25;i++)
        if(ac[0].nxt[i])q.push(ac[0].nxt[i]);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<=25;i++)
        if(ac[u].nxt[i]){
        q.push(ac[u].nxt[i]);
        int now=ac[u].fail;
        while(now&&ac[now].nxt[i]==0)now=ac[now].fail;
        if(now==0&&ac[now].nxt[i]==0)ac[ac[u].nxt[i]].fail=0;
            else ac[ac[u].nxt[i]].fail=ac[now].nxt[i];
        }
    }
}
void work(char *s){
    int l=strlen(s+1);
    int now=0;
    for(int i=1;i<=l;i++){
        while(now&&ac[now].nxt[s[i]-'a']==0)now=ac[now].fail;
        now=ac[now].nxt[s[i]-'a'];
        for(int y=now;ac[y].cnt!=-1;y=ac[y].fail){
            ans+=ac[y].cnt;
            ac[y].cnt=-1;
        }
    }
}
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        scanf("%s",s+1);
        insert(s);
    }
    get_fail();
    scanf("%s",s+1);
    work(s);
    printf("%d",ans);
    return 0;
}

Trie圖優化

假如節點 \(x\) 沒有字符 \(c\) 的出邊,那麼不妨找到 \(x\)\(fail\) 樹上最近的一個有字符 \(c\) 出邊的祖先,從 \(x\) 連一條邊到這個點的字符 \(c\) 的兒子。這些邊以及本來 \(Trie\) 樹的結構構成的轉移圖稱爲 \(Trie\) 圖。下面記 \(trans[x][c]\) 表示從節點 \(x\) 連出的字符 \(c\) 的出邊指向的節點。

Trie 圖則是至關於把 NFA 轉化爲了 DFA(這涉及到了自動機的概念,不瞭解也不影響對本篇文章的閱讀,有能力的能夠了解一下,能夠加深理解還能夠用來裝逼)

建圖的時候,\(fail[x]\) 就是 \(trans[fail[fa[x]]][c]\)。若是 \(x\) 自己有字符 \(c\) 的兒子,那麼 \(trans[x][c]\) 就是這個兒子,不然\(trans[x][c]\) = \(trans[fail[x]][c]\)

時間複雜度 \(O(|T||∑|)\),其中 \(|T|\) 表示 \(Trie\) 的大小,\(|∑|\)表示字符集大小。

下面是Trie圖優化的代碼,仍是剛纔那個模板題

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
const int N=1001000;
int n,ans,tot;
char s[N];
struct AC{
    int nxt[26],cnt,fail;
}ac[N];
void insert(char *s){
    int l=strlen(s+1);
    int now=0;
    for(int i=1;i<=l;i++){
        if(ac[now].nxt[s[i]-'a']==0)ac[now].nxt[s[i]-'a']=++tot;
        now=ac[now].nxt[s[i]-'a'];
    }
    ac[now].cnt++;
}
void get_fail(){
    queue<int> q;
    for(int i=0;i<=25;i++)
        if(ac[0].nxt[i])q.push(ac[0].nxt[i]);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<=25;i++)
            if(ac[u].nxt[i])q.push(ac[u].nxt[i]),ac[ac[u].nxt[i]].fail=ac[ac[u].fail].nxt[i];
            else ac[u].nxt[i]=ac[ac[u].fail].nxt[i];
    }
}
void work(char *s){
    int l=strlen(s+1);
    int now=0;
    for(int i=1;i<=l;i++){
        now=ac[now].nxt[s[i]-'a'];
        for(int y=now;ac[y].cnt!=-1;y=ac[y].fail){
            ans+=ac[y].cnt;
            ac[y].cnt=-1;
        }
    }
}
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        scanf("%s",s+1);
        insert(s);
    }
    get_fail();
    scanf("%s",s+1);
    work(s);
    printf("%d",ans);
    return 0;
}

本文只是講解算法,真正掌握它還須要多刷題。
完結撒花

相關文章
相關標籤/搜索