AC自動機講解+[HDU2222]Keywords Search(AC自動機)

首先,有這樣一道題:php

給你一個單詞W和一個文章T,問W在T中出現了幾回(原題見POJ3461)。html

OK,so easy~c++

HASH or KMP 輕鬆解決。算法

那麼還有一道例題:數組

給定n個長度不超過50的由小寫英文字母組成的單詞準備查詢,以及一篇長爲m的文章,問:文中出現了多少個待查詢的單詞(原題見POJ3630)。優化

OK,依然so easy~ui

字典樹(Trie)輕鬆解決。spa

那麼,若是你說,什麼是KMP和Trie,那麼恭喜你啊……指針

建議你們要在看這篇博客以前作到:htm


 

若是你能看到這裏,說明你已經熟練掌握了Trie和KMP。

那麼如今可愛的動動扔給你了一道題:

給定n個長度不超過50的由小寫英文字母組成的單詞準備查詢,以及一篇長爲m的文章,問:文中出現了多少個待查詢的單詞。多組數據。

 

那麼這個時候須要引入一個新的算法:AC自動機。

想必在所得諸位大佬必定有人聽過這個名詞。

首先簡要介紹一下AC自動機(Aho-Corasick automation)(不是Accepted)。
就像當初我前隊友gzh老師同樣(狀壓 or 撞鴨?)。

該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法之一。

要搞懂AC自動機,先得有模式樹(字典樹)Trie和KMP模式匹配算法的基礎知識。

KMP算法和AC自動機算法的區別在於,前者爲單模式串匹配,然後者爲多模式串匹配

你能夠感性的理解爲,單模式串匹配就是隻有一個子串,而多模式串匹配是指有不止一個子串。


 

主要步驟:

三步走:

①將全部的模式串構成一顆Trie樹。

②對Trie上全部的節點構造前綴指針。

③利用前綴指針Fail對主串進行匹配。

實際上這個前綴指針Fail與KMP算法中的nxt數組很是類似,所以AC自動機能夠看做是Trie與KMP算法的結合(Trie上的KMP算法)。

第一步:字典樹的構建想必你們都會了(若是不會的話你也不會讀到這裏),在此就不作過多的贅述。

第二步:

找Fail指針:

你們都知道,在KMP算法當中,當字符串發生失配時有nxt數組用來找到下一個匹配的位置,那麼AC自動機中相似nxt數組的東西就是fail指針,當發現失配的

字符失配的時候,跳轉到Fail指針指向的位置,而後再次進行匹配操做,AC自動機之因此能實現多模式匹配,就歸功於Fail指針的創建。

那麼咱們應該如何求Fail指針呢?

運用廣度優先搜索 (BFS)來求得。

對於與根節點直接相連的點來講,若是這些節點失配,他們的Fail指針直接指向root便可。

其餘節點其Fail指針求法以下:
設當前節點爲father,其子節點爲children。

求child的Fail指針時,首先咱們要找到其father的Fail指針所指向的節點設爲F,看F的孩子中有沒有和child節點所表示的字母相同的節點,若是有的話,這個節

點就是child的fail指針,若是沒有,則須要再找到F的Fail指針所指向的節點,若是一直找都找不到,則child的Fail指針就要指向root。

例(幫助理解):

如圖所示,首先root最初會進隊,而後root,出隊,咱們把root的孩子的失敗指針都指向root。所以圖中h,s的失敗指針都指向root,如紅色線條所示,同時h,s進隊。
接下來該h出隊,咱們就找h的孩子的fail指針,首先咱們發現h這個節點其fail指針指向root,而root又沒有字符爲e的孩子,則e的fail指針是空的,若是爲空,則也要指向root,如圖中藍色線所示。而且e進隊,此時s要出隊,咱們再找s的孩子a,h的fail指針,咱們發現s的fail指針指向root,而root沒有字符爲a的孩子,故a的fail指針指向root,a入隊,而後找h的fail指針,一樣的先看s的fail指針是root,發現root又字符爲h的孩子,因此h的fail指針就指向了第二層的h節點。e,a , h 的fail指針的指向如圖藍色線所示。
此時隊列中有e,a,h,e先出隊,找e的孩子r的失敗指針,咱們先看e的失敗指針,發現找到了root,root沒有字符爲r的孩子,則r的失敗指針指向了root,而且r進隊,而後a出隊,咱們也是先看a的失敗指針,發現是root,則y的fail指針就會指向root.而且y進隊。而後h出隊,考慮h的孩子e,則咱們看h的失敗指針,指向第二層的h節點,看這個節點發現有字符值爲e的節點,最後一行的節點e的失敗指針就指向第三層的e。最後找r的指針,一樣看第二層的h節點,其孩子節點不含有字符r,則會繼續往前找h的失敗指針找到了根,根下面的孩子節點也不存在有字符r,則最後r就指向根節點,最後一行節點的fail指針如綠色虛線所示。
第三步:

文本串的匹配:

匹配過程分兩種狀況:

(1)當前字符匹配,從當前節點沿着樹邊有一條路徑能夠到達目標字符,若是當前匹配的字符是一個單詞的結尾,就沿着當前字符的Fail指針,一直遍歷到根,若是這些節點末尾有標記(當前節點單詞末尾的標記),這些節點全都是能夠匹配上的節點。統計完畢後,並將那些節點標記。此時只需沿該路徑走向下一個節點繼續匹配便可,目標字符串指針移向下個字符繼續匹配;

(2)當前字符不匹配,則去當前節點失敗指針所指向的字符繼續匹配,當指針指向root時結束。

重複這2個過程當中的任意一個,直到模式串走到結尾爲止。

例:

仍是剛纔那張圖:

 

假設其模式串爲yasherhs。對於i=0,1。Trie中沒有對應的路徑,故不作任何操做;i=2,3,4時,指針p走到左下節點e。由於節點e的count信息爲1,因此cnt+1,而且講節點e的count值設置爲-1,表示改單詞已經出現過了,防止重複計數,最後temp指向e節點的失敗指針所指向的節點繼續查找,以此類推,最後temp指向root,退出while循環,這個過程當中count增長了2。表示找到了2個單詞she和he。當i=5時,程序進入第5行,p指向其失敗指針的節點,也就是右邊那個e節點,隨後在第6行指向r節點,r節點的count值爲1,從而count+1,循環直到temp指向root爲止。最後i=6,7時,找不到任何匹配,匹配過程結束。

總結:

三步:構造一棵Trie樹,構造失敗指針和模式匹配過程。

Fail指針≈nxt數組。

例題:

「一本通 2.4 例 1」Keywords Search
「一本通 2.4 練習 1」玄武密碼
「一本通 2.4 練習 3」單詞
「一本通 2.4 練習 5」病毒
「一本通 2.4 練習 4」最短母串
「一本通 2.4 練習 6」文本生成器


 例題1講解:

那麼下面我來說一下AC自動機的第一道例題Keywords Search。

題目傳送門

這是一道板子題,其實上面的內容就是根據這道題來說的,我在此就不作過多的贅述,主要講一下代碼。

 

#include<bits/stdc++.h>
using namespace std;
int n;
char s[2000001];
int trie[1000001][30];
int que[1000001],end[1000001],nxt[1000001];
int ans,cnt;
void insert(char *str)//Trie樹構建過程 
{
	int p=1;
	int len=strlen(str);
	for(int i=0;i<len;i++)
	{
		int ch=str[i]-'a';
		if(!trie[p][ch])
		{
			trie[p][ch]=++cnt;
			memset(trie[cnt],0,sizeof(trie[cnt]));//每次只須要清空咱們會用獲得的行 
		}
		p=trie[p][ch];
	}
	end[p]++;//由於有可能會有重複的單詞,故在此end統計在此有多少個單詞結束,而不是有沒有單詞結束 
}
void build()//BFS構建Fail指針 
{
	for(int i=0;i<26;i++)//爲了方便將0的全部轉一遍都設爲根節點1 
		trie[0][i]=1;
	nxt[1]=0;//若在根節點失配, 則沒法匹配字符 
	que[1]=1;
	int head=1,tail=1;
	while(head<=tail)
	{
		for(int i=0;i<26;i++)
			if(!trie[que[head]][i])trie[que[head]][i]=trie[nxt[que[head]]][i];//注意這裏,下面會有詳細解釋 
			else
			{
				que[++tail]=trie[que[head]][i];
				int flag=nxt[que[head]];
				while(flag&&!trie[flag][i])flag=nxt[flag];//循環往前找 
				nxt[trie[que[head]][i]]=trie[nxt[que[head]]][i];
			}
		head++;//注意隊頭++ 
	}
}
void find(char *str)//匹配 
{
	int p=1;
	int len=strlen(str);
	for(int i=0;i<len;i++)
	{
		int flag=p=trie[p][str[i]-'a'];
		while(end[flag]!=-1&&flag)
		{
			ans+=end[flag];
			end[flag]=-1;//標記這個點已經訪問過,之後再也不訪問 
			flag=nxt[flag];
		}
	}
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		memset(end,0,sizeof(end));//多測不清空,爆零兩行淚(寶寶別哭) 
		cnt=1;
		ans=0;
		for(int i=0;i<26;i++)
			trie[0][i]=1,trie[1][i]=0;//亦是清空 
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%s",s);
			insert(s);//讀入子串並插入Trie樹 
		}
		build();
		scanf("%s",s);
		find(s);//匹配 
		printf("%d\n",ans);
	}
	return 0;
}

 

下面我來解釋上面那個問題:

當發現不存在que[head](下面用u代替)的轉移邊i時,令trie[u][i]等於trie[nxt[u]][i],這並不符合Trie樹的構造,可是在代碼中倒是正確的,那麼這是爲何呢?

其實這是爲了優化時間,若不存在trie[u][i]的轉移邊則指向trie[nxt[u]][i]。由於在具體問題中,若不存在trie[u][i]的轉移邊,每每須要沿que[head]的Fail指針走到第一個知足存在字符i的轉移邊的點v,獲得trie[v][i],那麼就直接將trie[u][i]賦值爲trie[v][i],即trie[nxt[u]][i],這是求解的一類問題的時間優化。也正是這個緣由,在構建Fail指針是,並無處理v的轉移邊i不存在的狀況,而是直接nxt[trie[u][i]]=trie[v][i](其中trie[v][i]也在以前就處理好了)。


rp++

相關文章
相關標籤/搜索