1. 程式人生 > >HDU2222 多串匹配 (AC自動機)

HDU2222 多串匹配 (AC自動機)

AC自動機簡介: 

首先簡要介紹一下AC自動機:Aho-Corasick automation,該演算法在1975年產生於貝爾實驗室,是著名的多模匹配演算法之一。一個常見的例子就是給出n個單詞,再給出一段包含m個字元的文章,讓你找出有多少個單詞在文章裡出現過。要搞懂AC自動機,先得有字典樹Trie和KMP模式匹配演算法的基礎知識。KMP演算法是單模式串的字元匹配演算法,AC自動機是多模式串的字元匹配演算法。

AC自動機的構造:

1.構造一棵Trie,作為AC自動機的搜尋資料結構。

2.構造fail指標,使當前字元失配時跳轉到具有最長公共前後綴的字元繼續匹配。如同 KMP演算法一樣, AC自動機在匹配時如果當前字元匹配失敗,那麼利用fail指標進行跳轉。由此可知如果跳轉,跳轉後的串的字首,必為跳轉前的模式串的字尾並且跳轉的新位置的深度(匹配字元個數)一定小於跳之前的節點。所以我們可以利用 bfs在 Trie上面進行 fail指標的求解。

3.掃描主串進行匹配。

AC自動機詳講:

我們給出5個單詞,say,she,shr,he,her。給定字串為yasherhs。問多少個單詞在字串中出現過。

一、Trie

首先我們需要建立一棵Trie。但是這棵Trie不是普通的Trie,而是帶有一些特殊的性質。

首先會有3個重要的指標,分別為p, p->fail, temp。

1.指標p,指向當前匹配的字元。若p指向root,表示當前匹配的字元序列為空。(root是Trie入口,沒有實際含義)。

2.指標p->fail,p的失敗指標,指向與字元p相同的結點,若沒有,則指向root。

3.指標temp,測試指標(自己命名的,容易理解!~),在建立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為:


二、構造失敗指標

用BFS來構造失敗指標,與KMP演算法相似的思想。

首先,root入隊,第1次迴圈時處理與root相連的字元,也就是各個單詞的第一個字元h和s,因為第一個字元不匹配需要重新匹配,所以第一個字元都指向root(root是Trie入口,沒有實際含義)失敗指標的指向對應下圖中的(1),(2)兩條虛線;第2次進入迴圈後,從佇列中先彈出h,接下來p指向h節點的fail指標指向的節點,也就是root;p=p->fail也就是p=NULL說明匹配序列為空,則把節點e的fail指標指向root表示沒有匹配序列,對應圖-2中的(3),然後節點e進入佇列;第3次迴圈時,彈出的第一個節點a的操作與上一步操作的節點e相同,把a的fail指標指向root,對應圖-2中的(4),併入隊;第4次進入迴圈時,彈出節點h(圖中左邊那個),這時操作略有不同。由於p->next[i]!=NULL(root有h這個兒子節點,圖中右邊那個),這樣便把左邊那個h節點的失敗指標指向右邊那個root的兒子節點h,對應圖-2中的(5),然後h入隊。以此類推:在迴圈結束後,所有的失敗指標就是圖-2中的這種形式。


三、掃描

構造好Trie和失敗指標後,我們就可以對主串進行掃描了。這個過程和KMP演算法很類似,但是也有一定的區別,主要是因為AC自動機處理的是多串模式,需要防止遺漏某個單詞,所以引入temp指標。

匹配過程分兩種情況:(1)當前字元匹配,表示從當前節點沿著樹邊有一條路徑可以到達目標字元,此時只需沿該路徑走向下一個節點繼續匹配即可,目標字串指標移向下個字元繼續匹配;(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時,找不到任何匹配,匹配過程結束。

到此,AC自動機入門知識就講完了。HDU 2222入門題必須果斷A掉,反正我是參考別人程式碼敲的。。。

AC自動機貌似還有很多需要優化的地方,等把基礎搞定之後再學習一下怎麼優化吧。。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <map>
#include <queue>
using namespace std;

#define MAXN 275005
#define MOD 1000000 

#define LL long long int

struct node
{
	int v;
	node *next[26];
	node *fail;
}*root;

char S[1000005];
queue<node*> Q;

int main()
{
	int T, n;
	char s[55];
	node *p, *temp;
	scanf("%d", &T);
	while (T--)
	{
		//////////////////////建字典樹
		root = new node();
		scanf("%d", &n);
		for (int i = 0; i < n; ++i)
		{
			scanf("%s", s);
			int len = strlen(s);
			p = root;
			for (int i = 0; i < len; ++i)
			{
				int t = s[i] - 'a';
				if (p->next[t] == NULL)
					p->next[t] = new node();
				p = p->next[t];
			}
			p->v += 1;
		}

		/////////////////////完成fail指標
		Q.push(root);
		while (!Q.empty())
		{
			p = Q.front();
			Q.pop();
			for (int i = 0; i < 26; ++i)
			{
				if (p->next[i] != NULL)
				{
					Q.push(p->next[i]);
					if (p == root)
						p->next[i]->fail = root;
					else
					{
						temp = p->fail;
						while (temp != NULL)
						{
							if (temp->next[i] != NULL)
							{
								p->next[i]->fail = temp->next[i];
								break;
							}
							temp = temp->fail;
						}
						if (temp == NULL)
							p->next[i]->fail = root;
					}
				}
			}
		}

		//////////////////////搜尋
		scanf("%s", S);
		int len = strlen(S);
		int ans = 0;
		p = root;
		for (int i = 0; i < len; ++i)
		{
			int t = S[i] - 'a';
			temp = NULL;
			while (p->next[t] == NULL && p != root)
			{
				p = p->fail;
			}
			if (p == root)
			{
				if (p->next[t] != NULL)
				{
					temp = p = p->next[t];
				}
			}
			else
			{
				temp = p = p->next[t];
			}
			while (temp != NULL)
			{
				if (temp->v >= 0)
				{
					ans += temp->v;
					temp->v = -1;
					temp = temp->fail;
				}
				else
					temp = NULL;
			}
		}
		printf("%d\n", ans);

	}
}