[模板] AC自動機
[模板] AC自動機
AC 自動機是以 Trie 樹的結構 為基礎,結合 KMP 的思想進行的一種多模式匹配演算法。
典型應用是:用一個文字串來匹配多個模式串。
Trie 樹構建
和 Trie 樹模板沒有區別,還是要記錄模式串的結束位置。
放在 AC 自動機的演算法裡,一個結點表示一個字串 \(S\) 的字首,這也是這個模式串的一種狀態。
嚴格意義來說,應該把 Trie 樹的字元指標看成 邊 比較形象。
失配指標 fail
類似與 KMP 中的 \(next\) 陣列,我們把它們作下對比:
-
\(next\) 陣列記錄的是字串 \(S\) 的一個字首的 字首等於字尾的最大長度 。
-
\(fail\)
-
兩者都是為了在失配的時候進行跳轉用的。
構造思想 ,基本思想 。
設當前結點為 \(u\),\(p\) 為通過字元 \(c\) 指標的父親,考慮如何構造 \(u\) 結點的失配指標。
-
如果 \(t[fail[p]][c]\) 存在,那麼 \(fail[u]=t[fail[p][c]]\) ,類似於 KMP 中的繼承 \(nxt\) 陣列操作,這裡是通過繼承上一個狀態而得到了延續狀態的最長字尾。
-
如果上述結點不存在,向跳 \(next\) 陣列一樣跳 \(fail\)
具體實現就是一直跳 \(fail\),直到存在該結點為止。
放一張 \(oi-wiki\) 的圖理解一下:
其中橙色和紅色的邊代表失配指標。
至於我們為什麼要在 \(bfs\) 的時候處理 \(fail\) 指標,那就顯然了,那麼可以輕易得出結論:
**任意一個結點指向的 fail 指標的深度至少為它的深度 -1 **。
Trie 圖的構建
Trie 圖(字典圖),是在對原有的 Trie 結構的基礎上進行更改形成的 AC 自動機最後的圖。
又好寫又快
用來解決兩個事情:fail 指標的處理
同樣還是 bfs 更新子結點,在這裡我們不妨把 \(t[p][c]\) 看成從 \(S\) 後加一個字元 \(c\) 形成的 新狀態 。(即一個狀態轉移函式 \(trans(u,c)\))
類似於上面構造 \(fail\) 指標的過程,還是分兩種情況討論:
-
\(t[p][c]\) 存在,則讓 \(t[p][c]\) 的 \(fail\) 指標指向 \(t[fail[p]][c]\) 。
-
否則如果不存在這個轉移函式,我們就讓這個轉移函式 \(t[p][c]\) 指向 \(t[fail[p]][c]\) 。
這似乎有一個問題:我們之前在構建 \(fail\) 指標的時候得滿足合法才能停止跳 \(fail\) 更新子節點的 \(fail\) 指標。
可以說我們通過靈魂操作 2 保證了 比當前結點 \(t[p][c]\) 深度低的點的兒子都填滿了。
操作二的意義就是:我們之前可能會失配後跳 \(fail\) 指標多次才能來到下一個能夠匹配到的位置,但是通過操作二,可以讓失配的位置直接指向它下一個要匹配的位置。
這樣修改字典樹的結構,使得 匹配轉移更加完善 。同時它將 fail 指標跳轉的路徑做了壓縮(就像並查集的路徑壓縮),使得本來需要跳很多次 fail 指標變成跳一次。
這也是 Trie 圖常數小的原因。
再來看一張動圖:
黑色的邊代表在 Trie 樹上修改轉移函式得到的邊,黃色的邊代表 \(fail\) 指標。
如何理解這個東西?比如我們看四號結點的 \(trans(4,h)\) 更新情況。
先找到 \(fail[4]=7\) ,然後發現有 \(trans(7,h)\) 轉移函式指向 \(8\) 號結點,那麼 \(trans(4,h)\) 就是 \(8\) 號結點。
這個明顯是有意義的,比如當前文字串 \(T\) 匹配了 \(h-e-r-s\) ,接下來一個字元恰好是 \(h\) ,我們通過它的 \(trans(4,h)\) 轉移函式保證了一個儘量長的字尾。
小 \(tip\):不難發現自環只能發生在 \(bfs\) 的第一層。
void build(){
for(int i=0;i<26;i++){
if(t[0][i])q.push(t[0][i]);
}
while(q.size()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(t[u][i])fail[t[u][i]]=t[fail[u]][i],q.push(t[u][i]);
else t[u][i]=t[fail[u]][i];
}
}
}
多模式匹配操作
查詢操作在建完 Trie 圖後就簡單了,分為兩步:
-
在 Trie 圖(樹)上進行自我和樹的匹配。(因為兒子都被填滿了)
-
在匹配到的當前結點跳失配指標,並進行答案求解後清空標記陣列。
int query(char *s){
int u=0,res=0;
for(int i=1;s[i];i++){
u=t[u][s[i]-'a'];
for(int j=u;j && end[j]!=-1;j=fail[j]){
res+=end[j];end[j]=-1;
}
}
return res;
}
由於跳 \(fail\) 指標是唯一的,所以一個點 \(end[j]=-1\) 當且僅當 \(j\) 在 \(fail\) 指標的路徑上到根已經被打通了,這樣做可以保證複雜度。
還是來一張多模式匹配動圖:
可以發現,在匹配完 \(s-h-e\) 後失配 \(r\) 的時候,直接跳到了 \(3\) 的位置,這也是最佳位置。
板子們
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
template <typename T>
inline T read(){
T x=0;char ch=getchar();bool fl=false;
while(!isdigit(ch)){if(ch=='-')fl=true;ch=getchar();}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
}
return fl?-x:x;
}
#define read() read<int>()
const int maxn = 1e6 + 6;
int n;
#include <queue>
namespace AC{
int t[maxn][26],cnt,end[maxn],fail[maxn];
void insert(char *s){
int u=0;
for(int i=1;s[i];i++){
if(!t[u][s[i]-'a'])t[u][s[i]-'a']=++cnt;
u=t[u][s[i]-'a'];
}
end[u]++;
}
queue<int> q;
void build(){
for(int i=0;i<26;i++){
if(t[0][i])q.push(t[0][i]);
}
while(q.size()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(t[u][i])fail[t[u][i]]=t[fail[u]][i],q.push(t[u][i]);
else t[u][i]=t[fail[u]][i];
}
}
}
int query(char *s){
int u=0,res=0;
for(int i=1;s[i];i++){
u=t[u][s[i]-'a'];
for(int j=u;j && end[j]!=-1;j=fail[j]){
res+=end[j];end[j]=-1;
}
}
return res;
}
}
using namespace AC;
char s[maxn];
#define read() read<int>()
int main(){
n=read();
for(int i=1;i<=n;i++)scanf("%s",s+1),insert(s);
scanf("%s",s+1);
build();
printf("%d\n",query(s));
return 0;
}
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
template <typename T>
inline T read(){
T x=0;char ch=getchar();bool fl=false;
while(!isdigit(ch)){if(ch=='-')fl=true;ch=getchar();}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
}
return fl?-x:x;
}
#define read() read<int>()
const int maxn = 1e6 + 6;
int n;
#include <queue>
namespace AC{
int t[maxn][26],cnt,fail[maxn],val[maxn],idx[maxn],tot[200];
void insert(char *s,int id){
int u=0;
for(int i=1;s[i];i++){
if(!t[u][s[i]-'a'])t[u][s[i]-'a']=++cnt;
u=t[u][s[i]-'a'];
}
idx[u]=id;
}
queue<int> q;
void build(){
for(int i=0;i<26;i++){
if(t[0][i])q.push(t[0][i]);
}
while(q.size()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(t[u][i])fail[t[u][i]]=t[fail[u]][i],q.push(t[u][i]);
else t[u][i]=t[fail[u]][i];
}
}
}
inline void init(){
cnt=0;
memset(fail,0,sizeof fail);
memset(t,0,sizeof t);
memset(val,0,sizeof val);
memset(idx,0,sizeof idx);
memset(tot,0,sizeof tot);
}
int query(char *s){
int u=0,res=0;
for(int i=1;s[i];i++){
u=t[u][s[i]-'a'];
for(int j=u;j;j=fail[j]){
val[j]++;
}
}
for(int i=0;i<=cnt;i++)if(idx[i])res=max(res,val[i]),tot[idx[i]]=val[i];
return res;
}
}
using namespace AC;
char s[200][107],T[maxn];
#define read() read<int>()
int main(){
while(scanf("%d",&n)==1){
if(!n)break;
init();
for(int i=1;i<=n;i++)cin>>s[i]+1,insert(s[i],i);
build();
cin>>T+1;
int x=query(T);
printf("%d\n",x);
for(int i=1;i<=n;i++)if(tot[i]==x)printf("%s\n",s[i]+1);
}
return 0;
}
加強版唯一的區別就是需要記錄最多位置,因此不能用上一題的套路清空\(end\) 陣列了。(其實沒有 \(end\) 陣列)
- 感謝 OI-wiki 的資訊 。