AC自動機是一個多模字符串匹配的自動機(網上說的),主要作用是在一個長串中同時進行多個字符串的匹配
基礎芝士:
trie樹(字典樹)
烤饃片kmp單模字符串匹配
如果不會的建議去網上學一下(本篇講解略過)
這裏重點講一講AC自動機
(由於本蒟蒻不會指針,所以所有算法一律不使用指針,請神犇們諒解)
例:luogu3796 AC自動機(加強版)
其實AC自動機就是在trie樹上構造KMP的next指針(在AC自動機中叫fail指針),然後進行匹配
舉個例子:
模式串:
abab
abb
bab
匹配串:
aaabbbabababbba
AC自動機第一步:建立trie樹!
建樹過程略,反正建起的樹長這樣:
建樹代碼如下,基本和trie樹代碼接近:
void buildtree(char *p)
{
int l=strlen(p);
int now=0;
for(int i=0;i<l;i++)
{
int t=p[i]-'a'+1;
if(tree[now].to[t]==0)
{
tree[now].to[t]=++cnt;
tree[tree[now].to[t]].fa=now;
tree[tree[now].to[t]].ca=t;
}
now=tree[now].to[t];
}
tree[now].ed++;
}
接下來我們考慮構造fail指針
fail指針的含義其實就是:如果在這一位上失配了,那麼整個串不必從頭開始,而是直接從中間的某處開始繼續在失配處匹配即可
由於這是一棵trie樹,所以我們可以考慮基於bfs進行構造
首先,如果一開始就失配,那就沒啥可說的了,直接返回最大的根節點,所以在構造trie樹時一般從1開始,0作爲虛節點爲根
代碼如下:
queue <int> M;
for(int i=1;i<=26;i++)
{
if(tree[0].to[i])
{
M.push(tree[0].to[i]);
tree[tree[0].to[i]].fall=0;
}
}
接下來,我們就可以進行bfs了
這裏也是整個AC自動機中最複雜的地方
對於每個點,我們枚舉他的每一個to指針,然後分類討論:
①:這個to節點存在
(什麼叫存在?比如上面的trie樹,根據字符集來講,每個節點都應該有兩個兒子,可是事實上大部分節點都只有一個兒子,那麼有的這個兒子就叫存在,沒有就叫不存在)
那麼,這個to的fail指針應該指向他父節點的fail指針指向節點所指向的對應的to(讀二十遍)
先放代碼,再解釋,否則不好懂
if(tree[u].to[i])
{
tree[tree[u].to[i]].fall=tree[tree[u].fall].to[i];
M.push(tree[u].to[i]);
}
解釋一下,就像這樣:
其中藍色的線爲fail指針
發現什麼了嗎?
一個點fail指針所指向的點所在字符串的前綴一定是這個點所在字符串的子串!
舉個例子:
如圖所示,右邊紅色框裏的字符串的前綴是左邊紅色字符串的一個子串,因爲左邊的b指向了右邊的b
(當然,這個前綴理論僅適用於fail指針指向的節點之前的前綴,而之後的是無法保證的)
但是我們會發現一個bug:看到第二個串的最後一個b了嗎?他的fail指針應該指向他父節點的fail指針指向節點的對應節點,可是..沒有這個節點啊...
直接指回根節點?
這不太好
因爲明明有能匹配上的啊
所以我們要利用trie圖思想了。
trie圖與AC自動機少數的不同就是trie圖會補全所有的子節點,補全方法是指向這個點父節點的fail指針指向節點的對應節點
else
{
tree[u].to[i]=tree[tree[u].fall].to[i];
}
所以這也就是上面所述的分類討論的第二種情況:如果這個節點不存在,那麼要把這個節點的指針建起來
這樣就可以指了
最後構造好的fail指針長這樣:
其中綠色的是特殊構造出來的fail指針
fail指針都完事了,接下來就好辦了。
我們將模式串在這個AC自動機上跑
查詢操作:
int query(char *p)
{
int l=strlen(p);
int ans=0;
tot=0;
int now=0;
for(int i=0;i<l;i++)
{
int t=p[i]-'a'+1;
now=tree[now].to[t];
int temp=now;
while(temp)
{
if(tree[temp].ed>ans)
{
memset(ret,0,sizeof(ret));
tot=0;
ret[++tot]=temp;
ans=tree[temp].ed;
}else if(tree[temp].ed==ans)
{
ret[++tot]=temp;
}
if(tree[temp].ed)
{
tree[temp].ed++;
}
temp=tree[temp].fall;
}
}
return ans;
}
稍微解釋一下,就是順着trie樹跑匹配串,根據上文所述fail指針的性質,每次向前找一個前綴使得這個前綴是這個匹配串的子串,於是我們總是能找到整個串是這個字符串的子串
還有一步操作很重要,即上面的最後一個if,這一步的操作目的在於累計某個串被匹配上的次數
這樣就完事了
貼代碼:
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
struct Trie
{
int to[27];
int fa;
int fall;
int ca;
int ed;
}tree[1000005];
int ret[155];
char s[1000005];
int cnt=0;
int tot=0;
void buildtree(char *p)
{
int l=strlen(p);
int now=0;
for(int i=0;i<l;i++)
{
int t=p[i]-'a'+1;
if(tree[now].to[t]==0)
{
tree[now].to[t]=++cnt;
tree[tree[now].to[t]].fa=now;
tree[tree[now].to[t]].ca=t;
}
now=tree[now].to[t];
}
tree[now].ed++;
}
void getfail()
{
queue <int> M;
for(int i=1;i<=26;i++)
{
if(tree[0].to[i])
{
M.push(tree[0].to[i]);
tree[tree[0].to[i]].fall=0;
}
}
while(!M.empty())
{
int u=M.front();
M.pop();
for(int i=1;i<=26;i++)
{
if(tree[u].to[i])
{
tree[tree[u].to[i]].fall=tree[tree[u].fall].to[i];
M.push(tree[u].to[i]);
}else
{
tree[u].to[i]=tree[tree[u].fall].to[i];
}
}
}
}
int query(char *p)
{
int l=strlen(p);
int ans=0;
tot=0;
int now=0;
for(int i=0;i<l;i++)
{
int t=p[i]-'a'+1;
now=tree[now].to[t];
int temp=now;
while(temp)
{
if(tree[temp].ed>ans)
{
memset(ret,0,sizeof(ret));
tot=0;
ret[++tot]=temp;
ans=tree[temp].ed;
}else if(tree[temp].ed==ans)
{
ret[++tot]=temp;
}
if(tree[temp].ed)
{
tree[temp].ed++;
}
temp=tree[temp].fall;
}
}
return ans;
}
bool cmp(int a,int b)
{
return a<b;
}
void init()
{
memset(ret,0,sizeof(ret));
memset(tree,0,sizeof(tree));
cnt=0;
tot=0;
}
void print(int rt)
{
if(!rt)
{
return;
}
print(tree[rt].fa);
printf("%c",tree[rt].ca-1+'a');
}
int main()
{
int n;
while(1)
{
scanf("%d",&n);
if(n==0)
{
return 0;
}
init();
for(int i=1;i<=n;i++)
{
scanf("%s",s);
buildtree(s);
}
getfail();
scanf("%s",s);
printf("%d\n",query(s));
sort(ret+1,ret+tot+1,cmp);
for(int i=1;i<=tot;i++)
{
print(ret[i]);
printf("\n");
}
}
return 0;
}