AC快樂機
衆所周知,KMP是算法競賽中常用的字符串匹配算法,該算法通過對模式串構建next數組的方式,十分有效的提高了匹配的效率。
單一模式串的匹配可以構造next,那如果模式串有多個,也同樣能通過構造next的方式匹配嗎?
Fail指針
給你多個模式串,也就是給你一棵Trie,在Trie上進行匹配。假設我們有能力構建出一棵Trie的next,考慮KMP中的next的定義(指向最長的後綴),那麼一棵Trie的next是長這樣的(以模式串he,she,him,hers,shit爲例):
這棵Trie的next是這樣的(沒畫的都指向根):
現在要拿主串ashers上去匹配,匹配的方式和KMP一樣:如果可以接着向下走則向下走,否則爬next
那麼匹配的路徑爲root-root-s-h-e-e-r-s,最終匹配成功
匹配的複雜度顯然是的(其中爲主串長度,爲最長的模式串長度)
如果在Trie樹上能構造出next的話,匹配複雜度就會變成線性,這就是AC自動機基礎的原理啦,AC自動機中next的名字叫做Fail指針(也稱後綴節點),接下來的所有next都會用Fail代替
Fail指針是從一個Trie節點(對應字符串)到另一個Trie節點(對應的字符串是的最長後綴)的有向邊
匹配時,如果我們到達了不能繼續匹配的節點,我們沿着Fail指針爬以維持儘可能更大的前綴
對於Trie樹上每一個節點,我們都可以構造一個Fail指針
Fail指針的構造?
Fail指針的些許小性質:
1.根的所有兒子的Fail都是根
2.節點x的Fail指向y(非根),那麼x的t兒子的Fail指向y的t兒子的Fail
以上圖爲例,she
中h
的Fail指向的是he
中的h
,那麼sh
的e
兒子she
的Fail就會指h
的e
兒子he
3.一個節點一直沿着Fail爬會爬到根,期間深度是不斷減小的
這樣我們有了一個構建Fail的大體思路,對於每一個點,對他所有的兒子構建Fail,然後bfs下去,對t兒子構建Fail的過程可以通過不斷爬Fail找到第一個有t兒子的點這一途徑得到
但也會有一些不美妙的情況使這個過程變得很不美觀
假設上圖苟利國家生死以
的以
有一個撒
兒子,用剛纔所說的方法構造撒
的Fail指針就需要沿着藍色箭頭爬好遠好遠好遠好遠好遠好遠才能找到,這個過程會讓過程及複雜度極其不優美,AC自動機用Trie圖
簡化了這個過程
Trie圖
(爲了方便,將根所有不存在的兒子都設爲根)
對於Trie樹上的每一個節點,它的邊指向的是它下一個字符,表示如果當前狀態是,待匹配字符是,那麼匹配到的點是
設字符集爲,點可匹配的(向外延伸出去的)字符集,即,都可以(不通過爬Fail的方式)由向外延伸(到)
此過程描述爲:當的下一個字符是()時,走到的節點是
但是如果當前狀態是,下一個字符,也就代表着在此失配,那麼根據剛纔所描述的方法,AC自動機需要爬Fail找到第一個有兒子的點繼續匹配(到)。
此過程可以描述爲:當的下一個字符是()時,走到的節點是
這兩個過程描述起來十分相似,那麼我們爲什麼不將直接看作的兒子呢?
對於節點,如果它並沒有兒子,那麼沿着找到第一個有兒子的節點,並把的兒子看作是自己的兒子
我們把這個操作稱爲NTR,NTR過後的圖叫Trie圖
上圖理論上的Trie圖是長這樣的
但實際上是這樣的
如果NTR了的,NTR了的,可以直接看作NTR了的,也就是說如果我們按照Fail的順序構建,NTR操作只要考慮一個Fail就可以了
在Trie圖上的匹配只要無腦向後走就可以了,也就是說,構建完Trie圖,Fail就沒什麼用了
Trie圖本質上是個有向圖
Fail指針 Trie圖的構造!
struct Node{
Node *ch[26],*fail;
bool b;
Node():fail(NULL){
b=false;
for(int i=0;i<26;i++)
ch[i]=NULL;
}
}*root=new Node;
queue<Node*>q;
inline void Insert(char *s){///構建Trie
Node *x=root;
int len=strlen(s+1);
for(int i=1;i<=len;i++){
if(!x->ch[s[i]-'0']) x->ch[s[i]-'0']=new Node;
x=x->ch[s[i]-'0'];
}
x->b=true;
}
inline void GetFail(){///AC自動機可以以bfs的方式構建
root->fail=root;///根的Fail是他自己
for(int i=0;i<26;i++){
if(root->ch[i]) q.push(root->ch[i]),root->ch[i]->fail=root;
///將根兒子的Fail指向根,並放入隊列裏
else root->ch[i]=root;
///如果root沒有i這個兒子,就把他的兒子賦爲自己,這樣以上的性質2對根也滿足了
}
while(!q.empty()){
Node *x=q.front();q.pop();
for(int i=0;i<26;i++){
if(x->ch[i]) x->ch[i]->fail=x->fail->ch[i],q.push(x->ch[i]);
///根據性質2搞出各個兒子的Fail
else x->ch[i]=x->fail->ch[i];///NTR操作(
}
Node *tmp=x->fail;
while(tmp!=root && !tmp->b) tmp=tmp->fail;
if(tmp->b) x->b=true;
///如果它的某一後綴是一個模式串,那麼它也肯定包含模式串(一些特定的題統計時會用到)
}
}
在Trie圖上的匹配過程可以直接當成Trie樹上匹配,代碼也是一樣的
inline int Match(char *c){
int len=strlen(c+1);
int ans=0;
Node *x=root;
for(int i=1;i<=len;i++){
int k=c[i]-'a';
x=x->ch[k];///一直往下走就行了
Node *tmp=x;///這部分是統計的...
while(tmp!=root && !tmp->b){
ans+=tmp->num,tmp->b=true;
tmp=tmp->nex;
}
}
return ans;
}
Fail樹
把所有Fail指針當作一條無向邊,可以發現構成的是一個樹形結構
小例題:
給定一個n個單詞的文章,求每個單詞在文章中的出現次數 (,單詞總長)
大體思路:先將所有的串建一個AC自動機,構建的Fail時候到一個點就將所有它的所有Fail都加上1
顯然會T
可以發現,一個點一定是它Fail樹中子樹點的後綴(如果串s能匹配到這裏,s就會是子樹所有點的後綴)
那麼一個串的出現次數事實上就是它Fail樹子樹中單詞數量的和
然後沒了