AC自動機是一種多模匹配算法
AC自動機的關鍵是:構建字典圖實現自動跳轉,構建失配指針實現多模式匹配。
給定n個模式串和1個文本串,求有多少個模式串在文本串裏出現過。
玩命寫註釋
/*luogu P3808 【模板】AC自動機(簡單版)*/
struct Trie{//trie樹
int fail;//失配指針
int vis[30];//子結點的位置編號
//vis數組實際上就是trie樹裏的trie[p][ch],指向的是該結點的子結點的編號
int End;//trie樹裏用於標記有幾個單詞是以這個結點結尾的
}AC[N];
int tot=0;//AC自動機裏是從根節點0開始,trie樹一般是從根節點1開始
inline void build(string s){//構造tried樹//基本的trie樹的操作
int len = s.length();
int p = 0;//trie樹的當前指針(當前結點)從根節點0開始
for(int i = 0;i < len;++i){
if(AC[p].vis[s[i]-'a'] == 0)//tire樹的老規矩(不存在這個結點)
AC[p].vis[s[i]-'a'] = ++tot;//不存在就構造一個新結點
p = AC[p].vis[s[i]-'a'];//往下走
}
AC[p].End++;//標記每一個單詞的結尾//因爲最後要求的是有多少個單詞。要計算重複的
}
void Get_fail(){//構造失配指針利用bfs 遍歷
queue<int>Q;// 隊列存
for(int i= 0;i < 26;++i){//處理第二層的失配指針(26個字母遍歷一遍)
if(AC[0].vis[i] != 0){//若存在這個點
AC[AC[0].vis[i]].fail = 0;//第二層都要指向根節點
Q.push(AC[0].vis[i]);//並將該結點編號壓入隊列
}
}
while(!Q.empty()){//遍歷所有的結點
int u = Q.front();//每次取出隊頭
Q.pop();
for(int i = 0;i < 26;++i){//枚舉所有可能的子結點(26個字母)
if(AC[u].vis[i]!=0){//若存在這個字母的子結點
AC[AC[u].vis[i]].fail = AC[AC[u].fail].vis[i];//注意是vis[i],一定是指向與該結點字母相同的結點,若不存在就會是指向0(根節點)
//讓當前結點的子結點的失配指針fail指向,當前結點的失配指針fail所指向的
//結點的,與當前結點的子結點字母相同的子結點
Q.push(AC[u].vis[i]);
//並把該結點壓入隊列裏等待繼續往下遍歷
}
else//若不存在子結點
AC[u].vis[i] = AC[AC[u].fail].vis[i];
//因爲不存在該結點,本身在trie樹裏會直接結束,但是會浪費時間
//所以這裏本身沒有路可以走了,
//但是這裏讓當前結點的子結點直接指向當前結點的失配指針fail指向的結點,
//把路連上,下次遇上了可以直接走,這樣將trie樹構造成trie圖,更加的優化、
/////////////////////////////
//其實就是相當於先預處理好,因爲每一個點都要儘量連到另一個相同字母的結點,每一次遍歷可能儘管這個結點沒有這個字母
//但是先把他連到結點的失配指針指向的那一個結點,說不定就有這個字母
}
}
}
int AC_Query(string s){//AC自動機匹配
int len = s.length();
int p = 0,ans = 0;
for(int i = 0;i < len;++i){
p = AC[p].vis[s[i]-'a'];//往下一層走
for(int k = p;k&&AC[k].End!=-1;k = AC[k].fail){//往後走,若沒有到走過的結點或者到達根節點,就一直往失配指針指向的地方走
ans += AC[k].End;//加上路徑上的值
AC[k].End = -1;//標記已走過(已經加過一遍了)
}
}
return ans;
}
int n;
string s;
int main()
{
scanf("%d",&n);
over(i,1,n){
cin>>s;
build(s);//建trie樹(圖)
}
AC[0].fail = 0;//結束標誌(根節點的失配指針是一定指向自己的)
Get_fail();//求出失配指針
cin>>s;
cout<<AC_Query(s)<<endl;
return 0;
}