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,它父親的nxt的a這個兒子就是u的nxt。
還有一種情況,就是如果節點u,它的沒有a這個兒子,那么它就要把nxt[u]的a這個兒子當成他的兒子。
如圖,因為u沒有a的子節點,所以就連到nxt[u]的a子節點。
那么這么做的原因是什么?我們來看一下這個圖:
如圖,這個trie樹中前7個節點的next都已經構造完成了(箭頭表示他們的nxt,1的nxt是0,沒有畫出來).現在要找8的next。按照“它的nxt是,它父親的nxt的和它名字相同的兒子”的原則,我們找到8的父親,7,發現7的nxt,5也沒有B這個兒子,這時候我們需要找5的next,2,最終發現2有B兒子,是4,將8連到4。
但是注意,其實我們這一個一個找nxt是可以省略的。如果按着剛才“因為u沒有a的子節點,所以就連到nxt[u]的a子節點。”樹就會變成這樣(黑線表示連邊,紅線表示next)
5因為沒有B兒子,就把他的nxt:2,的B兒子:4,當成自己的兒子,7也同理,因為它沒有A兒子,所以把他的nxt的A兒子:2,當成自己的A兒子。再來看8,發現它的父親的nxt,5,的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這個兒子時,把自己的next的c這個兒子當做自己的兒子
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就彈出棧里面此字符串長度的元素。