字符串算法之 AC自己主動機
近期一直在學習字符串之類的算法,感覺BF算法,盡管非常easy理解,可是easy超時,全部就想學習其它的一些字符串算法來提高一下,近期學習了一下AC自己主動機。盡管感覺有所收獲,可是還是有些朦朧的感覺,在此總結一下,希望大家不吝賜教。
一、AC自己主動機的原理:
Aho-Corasick automaton。該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法之中的一個。
一個常見的樣例就是給出N個單詞,在給出一段包括m個字符的文章,讓你找出有多少個單詞在這文章中出現過,。要搞懂AC自己主動機。先的有字典樹和KMP模式匹配算法的基礎知識。
假設沒有kmp或者字典樹算法基礎的能夠看看:
<span style="font-size:18px;">//kmp http://blog.csdn.net/qq_16997551/article/details/51038525</span>
<span style="font-size:18px;">//字典樹 http://blog.csdn.net/qq_16997551/article/details/51107243</span>
二、AC自己主動機算法的實現步驟(三步)
AC自己主動機的存儲數據結構
const int MAXN = 10000000; struct node { int count; //是否為單詞最後一個節點 node *next[26];//Trie每一個節點的26個子節點 node *fail; //失敗指針 }; node *q[MAXN]; //隊列。採用bfs 構造失敗指針 char keyword[55];//輸入單詞 模式串 char str[1000010];// 須要查找的 主串 int head,tail;//隊列 頭尾指針
1、構造一棵Trie樹
首先我們須要建立一棵Trie。
可是這棵Trie不是普通的Trie,而是帶有一些特殊的性質。
首先會有3個重要的指針,分別為p, p->fail, temp。
1.指針p,指向當前匹配的字符。若p指向root,表示當前匹配的字符序列為空。
(root是Trie入口。沒有實際含義)。
2.指針p->fail,p的失敗指針,指向與字符p同樣的結點,若沒有。則指向root。
3.指針temp,測試指針(自己命名的。easy理解!~),在建立fail指針時有尋找與p字符匹配的結點的作用,在掃描時作用最大,也最不好理解。
對於Trie樹中的一個節點,相應一個序列s[1...m]。此時,p指向字符s[m]。若在下一個字符處失配,即p->next[s[m+1]] == NULL,則由失配指針跳到還有一個節點(p->fail)處,該節點相應的序列為s[i...m]。若繼續失配,則序列依次跳轉直到序列為空或出現匹配。在此過程中。p的值一直在變化,可是p相應節點的字符沒有發生變化。
在此過程中,我們觀察可知,終於求得得序列s則為最長公共後綴。另外。因為這個序列是從root開始到某一節點,則說明這個序列有可能是某些序列的前綴。
再次討論p指針轉移的意義。假設p指針在某一字符s[m+1]處失配(即p->next[s[m+1]] == NULL),則說明沒有單詞s[1...m+1]存在。
此時。假設p的失配指針指向root,則說明當前序列的隨意後綴不會是某個單詞的前綴。
假設p的失配指針不指向root,則說明序列s[i...m]是某一單詞的前綴,於是跳轉到p的失配指針。以s[i...m]為前綴繼續匹配s[m+1]。
對於已經得到的序列s[1...m],因為s[i...m]可能是某單詞的後綴,s[1...j]可能是某單詞的前綴,所以s[1...m]中可能會出現單詞。此時,p指向已匹配的字符,不能動。
於是。令temp = p。然後依次測試s[1...m], s[i...m]是否是單詞。
構造的Trie為:
實現代碼:
<span style="font-size:18px;">void insert(char *word,node *root) { int index,len; node *p = root,*newnode; len = strlen(word); for(int i=0 ;i < len ; i++ ) { index=word[i]-'a'; if(!p->next[index])//該字符節點不存在。增加Trie樹中 { // 初始化 newnode 並 增加 Trie 樹 newnode=(struct node *)malloc(sizeof(struct node)); for(int j=0;j<26;j++) newnode->next[j]=0; newnode->count=0; newnode->fail=0; p->next[index]=newnode; } p=p->next[index];//指針移動至下一層 } p->count++; //單詞結尾 節點 count + 1 做標記 }</span>
2、構造失敗指針
構造失敗指針的過程概括起來就一句話:設這個節點上的字母為x,沿著他父親的失敗指針走,直到走到一個節點,他的兒子中也有字母為x的節點。
然後把當前節點的失敗指針指向那個字符也為x的兒子。
假設一直走到了root都沒找到,那就把失敗指針指向root。
有兩個規則:
-
root的子節點的失敗指針都指向root。
-
節點(字符為x)的失敗指針指向:從X節點的父節點的fail節點回溯直到找到某節點的子節點也是字符x。沒有找到就指向root。
例如以下圖
實現代碼:
<span style="font-size:18px;">void build_ac_automation(node *root) { head=0; tail=1; q[head]=root; node *temp,*p; while(head<tail)//bfs構造 Trie樹的失敗指針 { //算法相似 kmp ,這裏相當於得到 next[]數組 //重點在於,匹配失敗時,由fail指針回溯到正確的位置 temp=q[head++]; for(int i=0;i< 26 ;i ++) { if(temp->next[i])//推斷實際存在的節點 { // root 下的第一層 節點 的 失敗指針都 指向root if(temp==root) temp->next[i]->fail=root; else { //依次回溯 該節點的父節點的失敗指針 //直到某節點的next[i]與該節點同樣。則 //把該節點的失敗指針指向該next[i]節點 //若回溯到 root 都沒有找到,則該節點 //的失敗指針 指向 root p=temp->fail;//temp 為節點的父指針 while(p) { if(p->next[i]) { temp->next[i]->fail=p->next[i]; break; } p=p->fail; } if(!p)temp->next[i]->fail=root; } //每處理一個點,就把它的全部兒子增加隊列, //直到隊列為空 q[tail++]=temp->next[i]; } } } }</span>
3、模式匹配過程
從root節點開始,每次依據讀入的字符沿著自己主動機向下移動。
當讀入的字符。在分支中不存在時,遞歸走失敗路徑。假設走失敗路徑走到了root節點, 則跳過該字符。處理下一個字符。 由於AC自己主動機是沿著輸入文本的最長後綴移動的,所以在讀取全然部輸入文本後,最後遞歸走失敗路徑,直到到達根節點, 這樣能夠檢測出全部的模式。
搜索的步驟:
-
從根節點開始一次搜索;
-
取得要查找關鍵詞的第一個字符。並依據該字符選擇相應的子樹並轉到該子樹繼續進行檢索;
-
在相應的子樹上,取得要查找關鍵詞的第二個字符,並進一步選擇相應的子樹進行檢索。
-
叠代過程……
-
在某個節點處。關鍵詞的全部字符已被取出,則讀取附在該節點上的信息,即完畢查找。
匹配模式串中出現的單詞。當我們的模式串在Trie上進行匹配時,假設與當前節點的keyword不能繼續匹配的時候。
就應該去當前節點的失敗指針所指向的節點繼續進行匹配。
匹配過程出現兩種情況:
-
當前字符匹配,表示從當前節點沿著樹邊有一條路徑能夠到達目標字符, 此時僅僅需沿該路徑走向下一個節點繼續匹配就可以 。目標字符串指針移向下個字符繼續匹配;
-
當前字符不匹配,則去當前節點失敗指針所指向的字符繼續匹配,匹配過程隨著指針指向root結束。
反復這2個過程中的隨意一個。直到模式串走到結尾為止。
實現代碼:
<span style="font-size:18px;">int query(node *root)//相似於 kmp算法。 {//i為主串指針,p為匹配串指針 int i,cnt=0,index,len=strlen(str); node *p=root; for(i=0; i < len ;i ++) { index=str[i]-'a'; //由失敗指針回溯尋找,推斷str[i]是否存在於Trie樹中 while( !p->next[index] && p != root) { p=p->fail; } p=p->next[index];//找到後 p 指向該節點 //指針回為空。則沒有找到與之匹配的字符 if(!p) { p=root;//指針又一次回到根節點root,下次從root開始搜索Trie樹 } node *temp=p;//匹配該節點後。沿其失敗指針回溯,推斷其它節點是否匹配 while(temp != root )//匹配 結束控制 { if(temp->count>=0)//推斷 該節點是否被訪問 { //統計出現的單詞個數cnt。因為節點不是單詞結尾時count為0。 //故 cnt+=temp->count; 僅僅有 count >0時才真正統計了單詞個數 cnt+=temp->count; temp->count=-1; //標記已訪問 } else break;//節點已訪問,退出循環 temp=temp->fail;//回溯失敗指針繼續尋找下一個滿足條件的節點 } } return cnt; }</span>
三、AC自己主動機模板
<span style="font-size:18px;">#include<stdio.h> #include<string.h> #include<stdlib.h> #define kind 26 const int MAXN = 10000000; struct node { int count; //是否為單詞最後一個節點 node *next[26];//Trie每一個節點的26個子節點 node *fail; //失敗指針 }; node *q[MAXN]; //隊列,採用bfs 構造失敗指針 char keyword[55];//輸入單詞 模式串 char str[1000010];// 須要查找的 主串 int head,tail;//隊列 頭尾指針 node *root; void insert(char *word,node *root) { int index,len; node *p = root,*newnode; len = strlen(word); for(int i=0 ;i < len ; i++ ) { index=word[i]-'a'; if(!p->next[index])//該字符節點不存在,增加Trie樹中 { // 初始化 newnode 並 增加 Trie 樹 newnode=(struct node *)malloc(sizeof(struct node)); for(int j=0;j<26;j++) newnode->next[j]=0; newnode->count=0; newnode->fail=0; p->next[index]=newnode; } p=p->next[index];//指針移動至下一層 } p->count++; //單詞結尾 節點 count + 1 做標記 } void build_ac_automation(node *root) { head=0; tail=1; q[head]=root; node *temp,*p; while(head<tail)//bfs構造 Trie樹的失敗指針 { //算法相似 kmp ,這裏相當於得到 next[]數組 //重點在於,匹配失敗時。由fail指針回溯到正確的位置 temp=q[head++]; for(int i=0;i< 26 ;i ++) { if(temp->next[i])//推斷實際存在的節點 { // root 下的第一層 節點 的 失敗指針都 指向root if(temp==root) temp->next[i]->fail=root; else { //依次回溯 該節點的父節點的失敗指針 //直到某節點的next[i]與該節點同樣。則 //把該節點的失敗指針指向該next[i]節點 //若回溯到 root 都沒有找到,則該節點 //的失敗指針 指向 root p=temp->fail;//temp 為節點的父指針 while(p) { if(p->next[i]) { temp->next[i]->fail=p->next[i]; break; } p=p->fail; } if(!p)temp->next[i]->fail=root; } //每處理一個點,就把它的全部兒子增加隊列。 //直到隊列為空 q[tail++]=temp->next[i]; } } } } int query(node *root)//相似於 kmp算法。 {//i為主串指針,p為匹配串指針 int i,cnt=0,index,len=strlen(str); node *p=root; for(i=0; i < len ;i ++) { index=str[i]-'a'; //由失敗指針回溯尋找,推斷str[i]是否存在於Trie樹中 while( !p->next[index] && p != root) { p=p->fail; } p=p->next[index];//找到後 p 指向該節點 //指針回為空。則沒有找到與之匹配的字符 if(!p) { p=root;//指針又一次回到根節點root,下次從root開始搜索Trie樹 } node *temp=p;//匹配該節點後,沿其失敗指針回溯,推斷其它節點是否匹配 while(temp != root )//匹配 結束控制 { if(temp->count>=0)//推斷 該節點是否被訪問 { //統計出現的單詞個數cnt,因為節點不是單詞結尾時count為0。 //故 cnt+=temp->count; 僅僅有 count >0時才真正統計了單詞個數 cnt+=temp->count; temp->count=-1; //標記已訪問 } else break;//節點已訪問,退出循環 temp=temp->fail;//回溯失敗指針繼續尋找下一個滿足條件的節點 } } return cnt; } int main() { int i,t,n,ans; scanf("%d",&t); while(t--) { root=(struct node *)malloc(sizeof(struct node)); for(int j=0;j<26;j++) root->next[j]=0; root->fail=0; root->count=0; scanf("%d",&n); getchar(); for(i=0;i<n;i++) { gets(keyword); insert(keyword,root); } build_ac_automation(root); gets(str); ans=query(root); printf("%d\n",ans); } return 0; }</span>
字符串算法之 AC自己主動機