題意(這裏可以點哦):有多個關鍵詞,在一個文中找到它們。
輸入:第一行是一個整數N,表示關鍵詞個數,下面有N個關鍵詞,N<=1000。每個關鍵詞只包含小寫字母,長度不超過50.最後一行是文本,長度不大於1000000。
輸出:在輸出文本中能找到多少關鍵詞。重複的關鍵詞只需要統計一次。
AC自動機可以說是KMP的升級版。KMP是單模匹配算法,處理在一個文本串中查找一個模式串的問題;AC自動機是多模匹配算法,能在一個文本串中同時查找多個不同的模式串。
- KMP算法是一種在任何情況下都能達到O(n+m)複雜度的算法,它的核心——Next[ ]數組(當出現失配後,進行下一次匹配時,用Next[ ]指出 j(模式串指針)回溯的位置)。KMP算法中的getFail()函數代碼如下:
void getFail(string s){//預計算Next[],用於在失配的情況下得到j回溯的位置
for(int i=1;i<s.size();++i){
int j=Next[i];
while(j&&s[j]!=s[i])j=Next[j];
Next[i+1]=(s[i]==s[j])?j+1:0;
}
}
其實KMP也能解決多模匹配問題,缺點就是複雜度較高,假設有k個模式串的平均長度爲m,那麼對每一個模式串分別做一次KMP,總複雜度是O((n+m)k)。
AC自動機算法不需要對S(文本串)做多次KMP,而是隻搜索一遍S,在搜索時匹配所有模式串。
KMP是通過查找P對應的Next[ ]數組實現快速匹配的。如果把所有的P做成一個字典樹,然後在匹配的時候查找這個P對應的Next[ ]數組,不就實現了快速匹配了嗎?建立字典樹是O(km),建立fail指針O(km),模式匹配O(nm),乘m的原因是在統計的時候需要順着鏈回溯到root節點。所以總時間複雜度是O((n+k)m),當m<<k時,(n+k)m<<(n+m)k,AC自動機的優勢就非常大了。
- 字典樹的代碼(用數組表示,需要一個Insert()函數)很簡單:
int trie[N][26],num[N],tot;//字典樹
void Insert(string s){
int u=0;
for(int i=0;i<s.size();++i){
int n=s[i]-'a';
if(trie[u][n]==0){
trie[u][n]=++tot;
}
u=trie[u][n];
}
num[u]++;//當前節點單詞數+1
}
那接下來就是AC自動機的getFail()函數,我們首先定義一個數組fail[ ](失配回溯指針),然後用bfs遍歷字典樹的所有節點來構造每個fail指針。重點是怎麼構造的,先堆公式:某節點的fail指針=父節點的fail指針指向節點的子節點(該節點代表的字母與某節點代表的字母相同) ,如果某節點不存在那就用它的鍵值代替它的fail指針,聽起來有點繞,但還好吧。 而公式的效果是什麼呢?你想想某節點的fail指針指向的節點和它有着相同的鍵值,那它的父節點fail指針指向的節點不也是同樣的,而第一層的節點的fail指針都指向根節點0,那我們就可以把fail指針指向的字符串(從根節點到fail指針指向的節點)看做是原字符串的一個子串還是最長的子串(此處子串都有一個特點:包含尾字符),所以我們循着fail指針的路徑可以遍歷所有有效判斷並返回根節點,簡單說就是通過遍歷文本串(文本串指針不需要回溯)來一堆一堆的判斷模式串是否存在。
- 如果還不明白,你就把模式串簡化成一個,你會發現AC自動機竟然模擬了KMP,是不是有所明悟。廢話就不多說了,直接上代碼:
//AC自動機
int fail[N];//失敗時的回溯指針
void getFail(){
queue<int>q;
for(int i=0;i<26;++i){
if(trie[0][i])
q.push(trie[0][i]);
}
while(!q.empty()){//bfs
int r=q.front();
q.pop();
for(int i=0;i<26;++i){
if(trie[r][i]){
//子節點失配回溯到父節點fail指針指向節點的下一個節點
fail[trie[r][i]]=trie[fail[r]][i];
q.push(trie[r][i]);
}
else {
//否則子節點指向父節點fail指針指向節點的下一個節點
trie[r][i]=trie[fail[r]][i];
}
}
}
}
- 最後就把題解代碼寫出來了,多了一個沒見過的query()函數,如果上述講解聽懂了,那很自然就理解了,函數中的循環就是遍歷子串的意思,僅此而已。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+1;
int trie[N][26],num[N],tot;//字典樹
void Insert(string s){
int u=0;
for(int i=0;i<s.size();++i){
int n=s[i]-'a';
if(trie[u][n]==0){
trie[u][n]=++tot;
}
u=trie[u][n];
}
num[u]++;//當前節點單詞數+1
}
//AC自動機
int fail[N];//失敗時的回溯指針
void getFail(){
queue<int>q;
for(int i=0;i<26;++i){
if(trie[0][i])
q.push(trie[0][i]);
}
while(!q.empty()){//bfs
int r=q.front();
q.pop();
for(int i=0;i<26;++i){
if(trie[r][i]){
//子節點失配回溯到父節點fail指針指向節點的下一個節點
fail[trie[r][i]]=trie[fail[r]][i];
q.push(trie[r][i]);
}
else {
//否則子節點指向父節點fail指針指向節點的下一個節點
trie[r][i]=trie[fail[r]][i];
}
}
}
}
int query(string s){
int sum=0,u=0;
for(int i=0;i<s.size();++i){
u=trie[u][s[i]-'a'];
for(int j=u;j&&num[j]!=-1;j=fail[j]){
sum+=num[j];
num[j]=-1;
}
}
return sum;
}
int main(){
int n;
string s;
cin>>n;
for(int i=1;i<=n;++i){
cin>>s;
Insert(s);
}
getFail();
cin>>s;
cout<<query(s)<<endl;
return 0;
}