【學習筆記】AC自動機
- 還沒學完,我先寫著
前置知識
kmp模式匹配、Trie
沒了
介紹:這玩意是幹什麼的
想必你第一次看到AC自動機這個名字,心潮湧動
其實這和做題AC啥關係沒有,這個AC是Aho-Corasick
你一定知道kmp是在一個文字串中匹配一個模式串
AC自動機(ACam/ACAM:Aho-Corasick automaton)可以讓你在一個文字串中匹配一堆模式串,但文字串只要掃一遍,很是NB
更通俗的解釋:看了也沒有幫助
正經板子
- 搞一個演算法,搞一個數據結構,都要從裸的搞起,學會了搞裸的,才能搞好別的。
裸T:
此處以3rd為例講解(這就是ACAM最經典的運用):
建立
AC自動機的實現結合Trie的結構和KMP的失配指標思想,構造過程可分為建立Trie與構造失配指標兩個部分:
如何建立Trie
和普通的Trie構建一模一樣,把所有模式串插入一個Trie(此處所用Trie陣列下文稱 \(tr\) )即可,此處不再贅述
為便於下文敘述,對於節點 \(x\) ,將Trie的根到\(x\)的路徑所表示的字串記為 \(S_{x}\)
構造失配指標
定義
對於節點 \(x\) ,其失配指標(下文稱 \(fail\) 指標)指向Trie中不為 \(x\) 的一個節點 \(y\) ,滿足 \(S_{y}\) 為 \(S_{x}\) 的字尾,且 \(S_{y}\) 最長
思想與步驟
大致參考KMP的思想,在對Trie進行搜尋(BFS)的過程中,設當前節點為\(x\)。
1.找到 \(x\) 的父親節點 \(p\) 向 \(x\) 的邊上字元 \(c\) (即 \(tr_{p,c}=x\) ),令 \(y=fail_{p}\)
2.若 \(tr_{p,c}\) 存在,則顯然 \(S_{tr_{p,c}}\) 為 \(S_{x}\) 字尾,又因每跳一次 \(fail\) ,\(p\) 的深度就會減小,故此時的 \(p\) 即為 \(fail_{x}\) ;若不存在,則 \(p \leftarrow fail_{p}\) ,並重復此步驟直至 \(p\)
字典圖
一個優化,對每個 \(tr_{x,c}\) ,若不為空則不變,若為空則 \(tr_{x,c} \leftarrow tr_{fail_{x},c}\)
這樣,對每個 \(tr_{x,c}\) 都有 \(fail_{tr_{x,c}}=tr_{fail_{x},c}\) ,節省了多餘空間,求 \(fail\) 陣列和匹配文字串時常數更小了,更容易寫了,大家都說好
code
inline void buildfail(){
int x;queue q;
for(re int i=0;i<26;++i)
if(tr[0][i]) q.push(tr[0][i]);
while(q.unempty()){
x=q.front(),q.pop();
for(re int i=0;i<26;++i)
if(tr[x][i]) q.push(tr[x][i]),fail[tr[x][i]]=tr[fail[x]][i];
else tr[x][i]=tr[fail[x]][i];
}
}
匹配
步驟
很明顯,ACAM建立以後,若匹配字串 \(T\) ,就一直對節點 \(x \leftarrow tr_{x,T_{i}}\) ,到達的 \(x\) 點就表示 \(S_{x}\) 為 \(T\) 的一個子串,記錄 \(x\) 被到達次數即可
但是還有一個東西
對於節點 \(x\) ,其失配指標指向Trie中不為 \(x\) 的一個節點 \(y\) ,滿足 \(S_{y}\) 為 \(S_{x}\) 的字尾,且 \(S_{y}\) 最長
也就是說,不止$ S_{x} $ 是 \(T\) 的一個子串, $ S_{fail_{x}} , S_{fail_{fail_{x}}} ......$ 都是 \(T\) 的子串
那怎麼辦?如果在每個節點迴圈跳 \(fail\) ,肯定會TLE ( 也可能卡過去,我沒試 )
但是,每個點的 \(fail\) 指標只有一個,並且根節點沒有 ( 或者說,在程式中為自己 ) !
也就是說, $ fail $ 為一顆樹,所以,匹配完以後,在fail樹上dfs或拓撲排序,就可以把 \(x\) 節點的資訊傳到它的 \(fail\) 上了!
最後對Trie圖上是模式串結尾的節點統計答案就好了
程式碼
inline void query(){
int i=0,x=0;scanf("%t",t+1);
while(t[++i]) x=tr[x][t[i]-'a'],++siz[x];
topo();
}
inline void topo(){
int x;queue q;
for(re int i=1;i<=cnt;++i) ++in[fail[i]];
for(re int i=1;i<=cnt;++i) if(!in[i]) q.push(i);
while(q.unempty()){
x=q.front();q.pop();
if(end[x]) ans[end[x]]+=siz[x];
siz[fail[x]]+=siz[x];
if(!(--in[fail[x]])) q.push(fail[x]);
}
}
一個小細節
此題有重複模式串,開鄰接表存一下就是
code
#include<cstdio>
#include<cstring>
#define re register
const int N=2e6+5,M=2e5+5;
int ans[M],nxt[M];
char s[N];
struct queue{
int l=1,r=0,a[M];
inline bool unempty(){return l<=r;}
inline int front(){return a[l];}
inline void pop(){++l;}
inline void push(int x){a[++r]=x;}
};
struct ACam{
int cnt,tr[M][26],end[M],fail[M],siz[M],in[M];
inline void ins(int k){
int i=0,x=0;scanf("%s",s+1);
while(s[++i]) x=tr[x][s[i]-'a']=(tr[x][s[i]-'a']?tr[x][s[i]-'a']:++cnt);
nxt[k]=end[x],end[x]=k;
}
inline void buildfail(){
int x;queue q;
for(re int i=0;i<26;++i)
if(tr[0][i]) q.push(tr[0][i]);
while(q.unempty()){
x=q.front(),q.pop();
for(re int i=0;i<26;++i)
if(tr[x][i]) q.push(tr[x][i]),fail[tr[x][i]]=tr[fail[x]][i];
else tr[x][i]=tr[fail[x]][i];
}
}
inline void topo(){
int x;queue q;
for(re int i=1;i<=cnt;++i) ++in[fail[i]];
for(re int i=1;i<=cnt;++i) if(!in[i]) q.push(i);
while(q.unempty()){
x=q.front();q.pop();
if(end[x]) ans[end[x]]+=siz[x];
siz[fail[x]]+=siz[x];
if(!(--in[fail[x]])) q.push(fail[x]);
}
}
inline void query(){
int i=0,x=0;scanf("%s",s+1);
while(s[++i]) x=tr[x][s[i]-'a'],++siz[x];
topo();
}
}ac;
int main(){
int n;scanf("%d",&n);
for(re int i=1;i<=n;++i) ac.ins(i);
ac.buildfail();ac.query();
for(re int i=n;i;--i) ans[nxt[i]]=ans[i];
for(re int i=1;i<=n;++i) printf("%d\n",ans[i]);
return 0;
}
一些題目
先鴿,Garrison叫我去打球