這篇博客我以hdu2222這道模版題爲例詳細的講解一下AC自動機。
AC自動機
簡介
AC自動機(Aho-Corasick automation),該算法在1975年產生于貝爾實驗室,是著名的多模匹配算法之一。
一個常見的例子就是給出n個單詞,再給出一段包含m個字符的文章,讓你找出有多少個單詞在文章裏出現過。
要學AC自動機最好先有KMP和字典樹Trie的基礎,AC自動機實際上就像是在Trie上進行KMP的算法。
字典樹Trie:http://blog.csdn.net/williamsun0122/article/details/71056547
KMP:http://blog.csdn.net/WilliamSun0122/article/details/75576868
思想
我們以5個單詞say,she,shr,he,her和字符串yasherhs爲例,問多少個單詞在字符串中出現過。
運用AC自動機的時候先構造Trie樹再運用KMP的思想即可。這裏的Trie樹有所不同,具體構造如下:
struct node{
node *next[maxn];
node *fail;
int num;
node()
{
for(int i=0;i<26;i++) next[i] = NULL;
fail = NULL;
num = 0;
}
};
可以看出多了fail指針。
這裏的fail指針就類似KMP裏面的next數組,是匹配失敗後的指向。
先把單詞插入到Trie樹中:
void ac_insert()
{
node *p = root;
for(int i=0;word[i];i++)
{
int id = word[i]-'a';
if(p->next[id]==NULL) p->next[id] = new node;
p = p->next[id];
}
p->num++;
}
構造Trie樹如圖
然後我們詳細講解一下AC自動機最重要的部分,就是fail失敗指針的求解。我們對着圖看我的講解。
我們是用BFS來構造fail指針,並且運用的是KMP的思想。
最開始先讓root入隊。隊列{root}
第1次循環時處理與root相連的字符即h和s。因爲第一個字符不匹配需要重新匹配,所以h和s的fail指針都指向root(root是Trie入口,沒有實際含義)即圖中(1)和(2)。此時隊列{h,s}
第二次循環時處理與h相連的字符即e。噹噹前字符e失配時應該將其fail指針指向當前字符串(he)的最長後綴和Trie樹中前綴的匹配(就是相當於指向其父節點h的fail指針指向的節點,看其有沒有當前節點e的匹配,有的話就指向它,沒有的話可以一直向上找直到fail指針指向root)。這裏h的fail指針指向root,而root沒有e的匹配,所以把e的fail指針指向root。此時隊列{s,e}
第三次循環時處理與s相連的字符a和h。其中a與第二次循環的e情況相同,將其fail指針指向root。h其父節點s的fail指針指向root,而root有h的匹配,所以把h的fail指針指向與root相連的h節點。
之後情況依此類推。
代碼如下:
void ac_setFail()
{
node *p,*tmp;
queue<node*> q;
q.push(root);
while(!q.empty())
{
p = q.front(),q.pop();
for(int i=0;i<26;i++)
{
if(p->next[i]==NULL) continue;
if(p==root)
{
p->next[i]->fail = root;
q.push(p->next[i]);
}
else
{
tmp = p->fail;
while(tmp)
{
if(tmp->next[i])
{
p->next[i]->fail = tmp->next[i];
break;
}
tmp = tmp->fail;
}
if(tmp==NULL) p->next[i]->fail = root;
q.push(p->next[i]);
}
}
}
}
最後就是AC自動機的多模匹配。匹配過程也與KMP的類似,並不是很難,我直接貼代碼,代碼裏面有一些註釋。
int ac_automation()
{
int ans=0;
node *p = root;
node *tmp;
for(int i=0;str[i];i++)
{
int id = str[i]-'a';
//噹噹前節點不是root且沒有該字符(id)的匹配就一直想上找
while(p->next[id]==NULL && p!=root) p = p->fail;
//找到有id的匹配不然就是root
if(p->next[id]!=NULL) p = p->next[id];
tmp = p;
//找到該匹配後要循環向上計數,不然可能會有遺漏。
//比如she和he不循環向上找的話就只會加she一個
while(tmp!=root)
{
if(tmp->num>0)
{
ans += tmp->num;
//計數後將其置0,避免重複計數
tmp->num = 0;
}
else tmp = tmp->fail;
}
}
return ans;
}
以上就是我對AC自動機的理解,希望對大家有幫助。
參考博客:http://blog.csdn.net/niushuai666/article/details/7002823
hdu2222
這題就是個模版題,看懂上面的內容之後就可以直接A了。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
int t,n;
char word[55],str[maxn];
struct node{
node *next[26];
node *fail;
int num;
node()
{
for(int i=0;i<26;i++) next[i] = NULL;
fail = NULL;
num = 0;
}
};
node *root;
void init()
{
root = new node;
}
void ac_insert()
{
node *p = root;
for(int i=0;word[i];i++)
{
int id = word[i]-'a';
if(p->next[id]==NULL) p->next[id] = new node;
p = p->next[id];
}
p->num++;
}
void ac_setFail()
{
node *p,*tmp;
queue<node*> q;
q.push(root);
while(!q.empty())
{
p = q.front(),q.pop();
for(int i=0;i<26;i++)
{
if(p->next[i]==NULL) continue;
if(p==root)
{
p->next[i]->fail = root;
q.push(p->next[i]);
}
else
{
tmp = p->fail;
while(tmp)
{
if(tmp->next[i])
{
p->next[i]->fail = tmp->next[i];
break;
}
tmp = tmp->fail;
}
if(tmp==NULL) p->next[i]->fail = root;
q.push(p->next[i]);
}
}
}
}
int ac_automation()
{
int ans=0;
node *p = root;
node *tmp;
for(int i=0;str[i];i++)
{
int id = str[i]-'a';
while(p->next[id]==NULL && p!=root) p = p->fail;
if(p->next[id]!=NULL) p = p->next[id];
tmp = p;
while(tmp!=root)
{
if(tmp->num>0)
{
ans += tmp->num;
tmp->num = 0;
}
else tmp = tmp->fail;
}
}
return ans;
}
int main()
{
scanf("%d",&t);
while(t--)
{
init();
scanf("%d",&n);
while(n--)
{
scanf("%s",word);
ac_insert();
}
ac_setFail();
scanf("%s",str);
printf("%d\n",ac_automation());
}
return 0;
}