最近學習AC自動機,看了不少講解AC自動機的文章,幾乎都是在講如何操作。估計不少人學習時像我一樣在想AC自動機演算法為什麼能實現多模式串匹配操作。如下是我的思考成果,如有漏洞,歡迎指正。
建立trie樹比較容易,構造fail指標其實是同樣的匹配過程,只要理解query()也就都明白了,下面主要來說說query()是如何完整地尋找出所有的模式串的。
對於給定的長字串,尋找有多少模式串在裡面出現過。query函數依次讀入長字串裡的字元,而匹配某一模式串操作是在query函數讀入長字串的某一字元時發生的。query依次讀入字元,不受其他動作影響(無論有沒有發生匹配query都老老實實地一個接一個地讀串)。每讀入長字串中的一個字元str[i],便需要求這樣一個子串,設為s[i](即str(k...i)串),滿足如下關係:
該子串是某一模式串的首碼,且是所有模式串中的最長首碼, 即不存在某字串的首碼為str(k1...i),k1<k。以下滿足該關係的子 串均簡稱為最長首碼(注意指的是模式串的最長首碼,不要混淆)。
現在要做的是每讀入一個str[i],求出它的最長首碼(s[i])。以下是求s[i]的方法:
s[i]=s[i-1]+str[i]-------------------->>>>當前配對的模式串的下一個字元==str[i](顯然s[i-1]表示上一狀態即掃描到str[i-1]的最長首碼);
s[i]=houzhui(houzhui...(houzhui(s(i-1))))+str[i]---------------->>>>當前配對的模式串的下一個字元!=str[i]
(houzhui()函數取的串滿足如下條件1當前配對串的尾碼;2其他模式串首碼;3滿足條件1、2的最長的串。描述起來很費勁,但你觀察已經建立好的trie樹和fail指標的特性,發現所謂houzhui()操作是水到渠成的(思考一下,其實trie樹有很多隱含特性的)。僅需第75、76行代碼。顯然,取尾碼操作是有截止條件的,截止條件就是當取得的尾碼(設為L(j...k))是某字串(設為L1(1...m))的首碼,且元素L1(k-j+1)==str[i],那麼L(j...k)+str[i]便是我們所求的s[i]。)
這個看起來像不像DP中的遞推關係式?把每一步求解str[i]看做一個狀態,每一步str[i]最長首碼s[i]的求解依賴於上一狀態str[i-1]的解,這應該就是AC自動機中的DP思想。
求解出每一狀態的最長首碼還遠沒有結束,現在我們知道了目前狀態下(即當前str[i]下)的最長首碼和str[i]這個字元,要求的是在這個str[i]下發生匹配的模式串。現在做如下討論:
若某一模式串在str[i]狀態下被匹配,則該模式串的末尾字元==str[i],且之前的字元是str[i]的最長首碼的某一尾碼。
下面就尋找滿足上述關係的模式串進行匹配操作,也就是依次尋找str[i]下的最長首碼的最長尾碼,該操作便是(之前看講解對此都是含糊不清的)。
while(temp!=root&&temp->count!=-1){cnt+=temp->count;temp->count=-1;temp=temp->fail;}
看是否有模式串恰好是那個str[i]下的最長首碼的尾碼。若有,則該串被匹配,不要停,繼續找尾碼,直到尾碼為0,即fail指向了root。(至於為什麼繼續找應該不需要解釋了)此處尋找操作再次用到了fail指標,fail指標的作用就是協助我們找尾碼,當然,準確地說是同樣出現在模式串中的尾碼。
貼出代碼(hdu2222):
#include<iostream>#include<cstring>#include<cstdio>using namespace std;struct node{node *fail;node *next[26];int count;}*q[500001]; //用作建trie樹時廣搜的隊列char keyword[51];char str[1000001];int head,tail;void insert(char str[],node *root){node *p=root;int i=0,cur;while(str[i]){cur=str[i]-'a';if(p->next[cur]==NULL)p->next[cur]=new node();p=p->next[cur];i++;}p->count++;}void build(node *root){int i;root->fail=NULL;q[head++]=root; //廣搜隊列while(head!=tail){node *temp=q[tail++];node *p=NULL;for(i=0;i<26;i++){if(temp->next[i]!=NULL){if(temp==root)temp->next[i]->fail=root;else{p=temp->fail;while(p!=NULL){if(p->next[i]!=NULL){temp->next[i]->fail=p->next[i];break;}p=p->fail;}if(p==NULL)temp->next[i]->fail=root;}q[head++]=temp->next[i]; //入隊列}}}}int query(node *root){int i=0,cnt=0,cur;node *p=root;while(str[i]){cur=str[i]-'a';while(p->next[cur]==NULL&&p!=root)p=p->fail;p=p->next[cur];p=(p==NULL)?root:p;node *temp=p;while(temp!=root&&temp->count!=-1){ //此操作見上述講解cnt+=temp->count;temp->count=-1;temp=temp->fail;}i++;}return cnt;}int main(){int t,n;scanf("%d",&t);while(t--){head=tail=0;node *root=new node();scanf("%d",&n);while(n--){scanf("%s",keyword);insert(keyword,root);}build(root);scanf("%s",str);printf("%d\n",query(root));}return 0;}