剛接觸AC自動機的時候覺得應該不難,可是~可是到了後來才覺得亞歷山大,在這裏,先撿最簡單的來說吧,對AC自動機的認識及AC自動機模版。
首先,我們需要對trie樹和KMP有初步瞭解,那麼我們先回顧一下:
KMP
首先這個匹配算法,主要思想就是要充分利用上一次的匹配結果,找到匹配失敗時,模式串可以向前移動的最大距離。這個最大距離,必須要保證不會錯過可能的匹配位置,因此這個最大距離實際上就是模式串當前匹配位置的next數組值。也就是max{Aj 是 Pi 的後綴 j < i},pi表示字符串A[1...i],Aj表示A[1...j]。模式串的next數組計算則是一個自匹配的過程。也是利用已有值next[1...i-1]計算next[i]的過程。我們可以看到,如果A[i] = A[next[i-1]+1]
那麼next[i] = next[i-1],否則,就可以將模式串繼續前移了。
trie樹
首先trie樹實際上就是一些字符串組成的一個字符查找樹,邊由代表組成字符串的字符代表,這樣我們就可以在O(len(str))時間裏判斷某個字符串是否屬於該集合。trie樹的節點內分支可以用鏈表也可以用數組實現,各有優劣。
簡單的trie樹每條邊由一個字符代表,但是爲了節省空間,可以讓邊代表一段字符,這就是trie的壓縮表示。通過壓縮表示可以使得trie的空間複雜度與單詞節點數目成正比。
可是,若是多模式匹配呢?如果是一堆單詞問是否在一篇文章中出現過,那麼用KMP顯然很耗時,而用trie樹又解決不了問題,於是,我們講kmp和trie圖結合起來,形成可如今的AC自動機。
最開始我還以爲是可以自動AC的一種機制,誰知,這竟然是兩個人名字(Aho-Corasick)的縮寫,這叫我等屌絲情何以堪?
廢話少說,ac自動機,可以看成是kmp在多字符串情況下擴展形式,可以用來處理多模式串匹配。只要爲這些模式串建立一個trie樹,然後再爲每個節點建立一個失敗指針,也就是類似與kmp的next函數,讓我們知道如果匹配失敗,可以再從哪個位置重新開始匹配。
所以,步驟:
1、建立trie樹
2、構造fail指針
3、遍歷文章
應該還記得,在kmp構造next數組時,我們是從前往後構造,即先構造1...i-1,然後再利用它們計算next[i],這裏也是類似。不過這個先後,是通過bfs的順序來體現的。AC自動機的失敗指針具有同樣的功能,也就是說當我們的模式串在Tire上進行匹配時,如果與當前節點的關鍵字不能繼續匹配的時候,就應該去當前節點的失敗指針所指向的節點繼續進行匹配。而從根到這個失敗指針指向的節點組成的字符串,實際上就是跟當前節點的後綴的匹配最長的字符串。
構造fail指針的過程:
如同KMP中模式串得自我匹配一樣.從根節點開始,對於每個結點:設該結點上得字符爲k,沿着其父親結點得失敗指針走,直到到達根節點或者當前失敗指針結點也存在字符爲k得兒子結點,那麼前一種情況當然是把失敗指針設爲根節點,而後一種情況則設爲當前失敗指針結點得字符爲k得兒子結點.
我們也可以動手操作一下,如果我們的ac自動機只包含一個模式串,這個過程實際上就是kmp的計算過程。
接下來要做的就是進行文本匹配:
首先,Trie-(模式串集合)中有一個指針p1指向head,而文本串中有一個指針p2指向串頭。下面的操作和KMP很類似:如果設k爲p2指向的字母 ,而在Trie中p1指向的節點存在字符爲k的兒子,那麼p2++,p1則改爲指向那個字符爲k的兒子,否則p1順着當前節點的失敗指針向上找,直到p1存在一個字符爲k的兒子,或者p1指向根結點。如果p1路過一個標記爲模式串終點的結點,那麼以這個點爲終點的的模式串就已經匹配過了.或者如果p1所在的點可以順着失敗指針走到一個模式串的終結結點,那麼以那個結點結尾的模式串也已經匹配過了。
好了,下面上圖<圖是Z神畫的>:
嗯,那麼fail指針如何用呢?通過這棵樹搜索時,若發現當前點的孩子中沒有當前所判斷的字符...就看當前點的Fail點的孩子有無,一直往上Fail直到找到一個點其孩子有所判斷的字符或者說找到了根都找不到符合的,就只能說這個字符不屬於任何一個單詞,只能繼續搜下一個字符。
好,來說說本題,題意就是給定N個單詞和一個文章,問在文章中出現過幾個已給定的單詞<出現過就不算了>。
#include<iostream>
#include<stdio.h>
#include<queue>
using namespace std;
struct node{
node *fail,*s[27];
int w;
}*head;
int t,n,i,j,sum;
char temp[51],str[1000001];
queue<node*> myqueue;
node *getfail(node *p,int k){
if (p->s[k]!=NULL) return p->s[k];
else
if (p==head) return head;
else return getfail(p->fail,k);
}
void built_trie(){
node *root=head;
node *tep;
for (j=0;j<strlen(temp);j++){
if (root->s[temp[j]-'a']==NULL){
tep=new node;
for (int k=0;k<26;k++)
tep->s[k]=NULL;
tep->w=0;
tep->fail=head;
root->s[temp[j]-'a']=tep;
}
root=root->s[temp[j]-'a'];
if (j==strlen(temp)-1) root->w+=1;
}
return ;
}
void built_ac(){
node *root;
while (!myqueue.empty()) myqueue.pop();
myqueue.push(head);
while (!myqueue.empty()){//構造fail指針用BFS的方法擴展
root=myqueue.front();
myqueue.pop();
for (j=0;j<26;j++)
if (root->s[j]!=NULL){
myqueue.push(root->s[j]);
if (root==head) root->s[j]->fail=head;
else root->s[j]->fail=getfail(root->fail,j);
}
}
return ;
}
void find(){
int len=strlen(str);
node* tep;
node *root=head;
for (j=0;j<len;j++){
while (root->s[str[j]-'a']==NULL && root!=head) root=root->fail;
root=(root->s[str[j]-'a']==NULL)?head:root->s[str[j]-'a'];
tep=root;
while (tep!=head){//如果單詞A出現過了,那麼與他具有相同前綴的單詞也都出現過了
sum+=tep->w;
tep->w=0;
tep=tep->fail;
}
}
return ;
}
int main(){
scanf("%d",&t);
while (t--){
sum=0;
head=new node;
for (i=0;i<26;i++)
head->s[i]=NULL;
head->fail=head;
head->w=0;
scanf("%d",&n);
for (i=0;i<n;i++){
scanf("%s",temp);
built_trie();
}
built_ac();
scanf("%s",str);
find();
printf("%d\n",sum);
}
return 0;
}