AC自動機詳解(附加可持久化AC自動機)


AC自動機

AC自動機,說白了就是在trie樹上跑kmp(其實個人感覺比kmp容易理解)。是一種多匹配串,單個主串的匹配。概括來說,就是將多個匹配串構造一個trie樹,對於每個trie樹的節點構造nxt指針,最后把主串放在上面跑。

 

構造trie

和普通的trie樹構建一樣,沒有什么區別

inline void insert(char *s){
     int l=strlen(s);
     int u=1;
     REP(i,0,l-1){
              int c=calc(s[i]);
              if(!tree[u][c])  tree[u][c]=++total;
              u=tree[u][c];
     }
     isend[u]++;//注意isend的具體處理根據題目而定
     return ;
}

 

構造nxt數組

其實這一部分是AC自動機的核心,我們這樣構造:對於每個節點,它的nxt是,它父親的nxt的和它名字相同的兒子。如圖,u的父親是v,它父親的nxta這個兒子就是unxt

還有一種情況,就是如果節點u,它的沒有a這個兒子,那么它就要把nxt[u]a這個兒子當成他的兒子。

如圖,因為u沒有a的子節點,所以就連到nxt[u]a子節點。

那么這么做的原因是什么?我們來看一下這個圖:

 

如圖,這個trie樹中前7個節點的next都已經構造完成了(箭頭表示他們的nxt1nxt0,沒有畫出來).現在要找8next。按照“它的nxt是,它父親的nxt的和它名字相同的兒子”的原則,我們找到8的父親,7,發現7nxt5也沒有B這個兒子,這時候我們需要找5next2,最終發現2B兒子,是4,將8連到4

但是注意,其實我們這一個一個找nxt是可以省略的。如果按着剛才“因為u沒有a的子節點,所以就連到nxt[u]a子節點。”樹就會變成這樣(黑線表示連邊,紅線表示next

5因為沒有B兒子,就把他的nxt2,的B兒子:4,當成自己的兒子,7也同理,因為它沒有A兒子,所以把他的nxtA兒子:2,當成自己的A兒子。再來看8,發現它的父親的nxt5,的B兒子是4,所以自己的next就是4了。這樣減少了剛才一個一個找nxt的步驟。

 

inline void getnxt(){//整個代碼用BFS實現
     while(!Q.empty()) Q.pop();
     REP(i,0,25)  tree[0][i]=1;//一個非常重要的細節處理,我們加一個虛擬節點0,並將它的所有邊都連到1,方便以后的運算
     nxt[1]=0;
     Q.push(1);
     while(!Q.empty()){
              int u=Q.front();//u是當前點,這時候nxt[u]已經處理過了,要處理的是u的兒子的nxt,也就是nxt[tree[u][i]]
              Q.pop();
              REP(i,0,25){//枚舉u節點的每一個子節點
                       if(!tree[u][i])  tree[u][i]=tree[nxt[u]][i];//這就是剛才說的很重要的一步優化, 如果自己沒有這個子節點,就把自己next的這個子節點當做自己的子節點。
                       else{
                           nxt[tree[u][i]]=tree[nxt[u]][i];//自己兒子的nxt等於自己nxt的兒子,這句話和“自己的nxt是,自己父親的nxt的和它名字相同的兒子”的意思相同,只是主語從待更新節點變成已就更新節點。
                           Q.push(tree[u][i]);
                       }
              }
    }
    return ;
}

 

查找

  查找的具體實現是根據題目而定,我就拿這道題舉個例子:給一大堆匹配串和一個主串,求有多少個匹配串在主串上出現過。

  這種題的做法就是現在構建trie樹的時候,把每個單詞的結尾都記錄一下:isend[i]++。最后跑一遍AC自動機,到每一個節點是ans+=isend[i];isend=0;這樣聽起來很簡單,那么怎么遍歷AC自動機呢?

循環遍歷主串s,令u表示當前點,每當主串s到下一位時,u=tree[u][s[i]-‘a’](就是等於它的兒子)。然后對於每個u,循環它的nxt直到根。每到一個點就ans+=isend。具體看代碼:

inline void search(){
     int ans=0;
     int u=1;
     int l=strlen(t);
     REP(i,0,l-1){//循環遍歷主串
              int c=calc(t[i]);//計算這個字符的ACCII碼
              int k=tree[u][c];
              while(k>1){//對於每一個u遍歷它的nxt,直到根
                       if(isend[k]){
                           ans+=isend[k];//加上isend,記錄答案
                           isend[k]=0;
                       }
                       k=nxt[k];
              }
              u=tree[u][c];//遍歷到它的兒子。
     }
     printf("%d\n",ans);

}

 

總結

再來回顧一下AC自動機的步驟:構建trie樹,構建next數組,查找。其中next有兩個原則:1、當這個節點沒有字符c這個兒子時,把自己的nextc這個兒子當做自己的兒子

2、自己兒子的nxt等於自己nxt的兒子

附上代碼:#include <iostream>

#include <cstdio> #include <algorithm> #include <cstring> #include <cmath> #include <cstdlib> #include <queue> #include <stack> #include <vector>
using namespace std; #define MAXN 100010
#define INF 10000009
#define MOD 10000007
#define LL long long
#define in(a) a=read()
#define REP(i,k,n) for(int i=k;i<=n;i++)
#define DREP(i,k,n) for(int i=k;i>=n;i--)
#define cl(a) memset(a,0,sizeof(a)) inline int read(){ int x=0,f=1;char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f; } inline void out(int x){ if(x<0) putchar('-'),x=-x; if(x>9) out(x/10); putchar(x%10+'0'); } int T,n; int total=1; int nxt[1000010],tree[500010][26]; char in[55]; int isend[1000010]; char t[1000010]; queue <int> Q; int calc(char c){ return c-'a'; } inline void insert(char *s){ int l=strlen(s); int u=1; REP(i,0,l-1){ int c=calc(s[i]); if(!tree[u][c])  tree[u][c]=++total; u=tree[u][c]; } isend[u]++; return ; } inline void getnxt(){//整個代碼用BFS實現
         while(!Q.empty()) Q.pop(); REP(i,0,25)  tree[0][i]=1;//一個非常重要的細節處理,我們加一個虛擬節點0,並將它的所有邊都連到1,方便以后的運算 nxt[1]=0; Q.push(1); while(!Q.empty()){ int u=Q.front();//u是當前點,這時候nxt[u]已經處理過了,要處理的是u的兒子的nxt,也就是nxt[tree[u][i]] Q.pop(); REP(i,0,25){//枚舉u節點的每一個子節點
                            if(!tree[u][i])  tree[u][i]=tree[nxt[u]][i];//這就是剛才說的很重要的一步優化, 如果自己沒有這個子節點,就把自己next的這個子節點當做自己的子節點。
                            else{ nxt[tree[u][i]]=tree[nxt[u]][i];//自己兒子的nxt等於自己nxt的兒子,這句話和“自己的nxt是,自己父親的nxt的和它名字相同的兒子”的意思相同,只是主語從待更新節點變成已就更新節點。 Q.push(tree[u][i]); } } } return ; } inline void search(){ int ans=0; int u=1; int l=strlen(t); REP(i,0,l-1){//循環遍歷主串
                   int c=calc(t[i]);//計算這個字符的ACCII碼
                   int k=tree[u][c]; while(k>1){//對於每一個u遍歷它的nxt,直到根
                            if(isend[k]){ ans+=isend[k];//加上isend,記錄答案 isend[k]=0; } k=nxt[k]; } u=tree[u][c];//遍歷到它的兒子。 } printf("%d\n",ans); } int main(){ in(T); while(T--){ total=1; cl(nxt); cl(tree); cl(isend); in(n); REP(i,1,n){ scanf("%s",in); insert(in); } scanf("%s",t); getnxt(); search(); } return 0; } 

 

附加:可持久化AC自動機

 

  如果你希望每當你查找到一個字符串,然后要把它刪去時,就需要可持久化AC自動機。其實和普通的AC自動機很像,唯一區別是查找的時候去掉了對於每一個u遍歷nxt直到根的步驟,然后讓每個u都壓進棧,遇到end就彈出棧里面此字符串長度的元素。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM