學習筆記——AC自動機
前言
$AC$自動機的題相對而言較為套路,但重在理解其思維,瞭解每一個數組的含義及一些拓展用法,就可以了。(模板一定要打對,不然真就成$WA$自動機了)
$AC$自動機的一些概念
我們都知道,$KMP$用於解決單模式串與多文字串的匹配問題,$Trie$樹用於實現字串快速檢索,$AC$自動機就是兩者的結合,用於解決多模式串匹配問題。
首先,我們可以仿照$Trie$樹,將每一個模式串都插入到$Trie$樹上,再在$Trie$樹上建立失配陣列,然後按照正常的$Trie$樹檢索和$KMP$匹配失配指標的方式和文字串進行匹配即可。
1.建立$AC$自動機
char str[N]; int trie[N][26],end[N],fail[N],cnt=0; //trie就是trie樹的陣列,end就是記錄結尾節點的陣列,fail就是失配陣列 il void insert() { scanf("%s",str+1); int id=0,len=strlen(str+1); for(re int i=1;i<=len;++i) { int to=str[i]-'a'; if(!trie[id][to]) trie[id][to]=++cnt; id=trie[id][to]; } ++end[id]; } //同Trie的建樹操作
2.構建失配陣列
il void get_fail() { queue <int> q;//存Trie樹上的節點 for(re int i=0;i<26;++i) { if(trie[0][i]) //如果有這個點,就加入佇列 q.push(trie[0][i]); //如果trie樹的起點不是0,是st,還要加一句fail[trie[0][i]]=st } while(!q.empty()) { int u=q.front();q.pop(); for(re int i=0;i<26;++i) { if(trie[u][i]) { fail[trie[u][i]]=trie[fail[u]][i]; //從u節點更新其兒子節點的失配指標 //其兒子節點的失配指標指向u節點的失配指標指向的節點的邊權相同的節點 //此處不用一直跳fail指標,因為u節點的fail指標更新先於其兒子的fail指標 q.push(trie[u][i]); //有這個點,就加入佇列 } else trie[u][i]=trie[fail[u]][i]; //沒有這個點,就把這個點變為u節點失配指標指向的邊權相同的點 } } }
這一步是$AC$自動機的精髓,光看程式碼可能還是比較難理解,下面給張圖解釋一下:
這是我們插入$Trie$樹中的文字串
這裡的虛線就是該節點的失配指標,類似$KMP$,當匹配失配時,我們可以通過跳$fail$來重新匹配,將字串匹配複雜度降低。(還是不會用畫圖軟體,將就看吧)
3.查詢操作
以下程式碼為查詢模式串在文字串中的出現次數。
il int ask() { scanf("%s",str+1);//輸入文字串 int id=0,len=strlen(str+1),res=0; for(re int i=1;i<=len;++i) { int to=str[i]-'a'; id=trie[id][to]; for(re int j=id;j&&end[j]!=-1;j=fail[j]) //跳fail指標,看是不是一個模式串的結束節點 res+=end[j],end[j]=-1; //是就統計答案,並清空防止以後再被訪問到 } return res; }
下面就是$AC$自動機的完整程式碼了:
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <queue>
#define il inline
#define ll long long
#define int long long
#define re register
#define gc getchar
using namespace std;
//------------------------初始程式--------------------------
il int read(){
re int x=0;re bool f=0;re char ch=gc();
while(!isdigit(ch)){f|=ch=='-';ch=gc();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
return f?-x:x;
}
il int max(int a,int b){
return a>b?a:b;
}
il int min(int a,int b){
return a<b?a:b;
}
//------------------------初始程式--------------------------
const int N=1e6+10;
int n;
char str[N];
int trie[N][26],end[N],fail[N],cnt=0;
//trie就是trie樹的陣列,end就是記錄結尾節點的陣列,fail就是失配陣列
il void insert() {
scanf("%s",str+1);
int id=0,len=strlen(str+1);
for(re int i=1;i<=len;++i) {
int to=str[i]-'a';
if(!trie[id][to]) trie[id][to]=++cnt;
id=trie[id][to];
}
++end[id];
}
il void get_fail() {
queue <int> q;//存Trie樹上的節點
for(re int i=0;i<26;++i) {
if(trie[0][i]) //如果有這個點,就加入佇列
q.push(trie[0][i]);
//如果trie樹的起點不是0,是st,還要加一句fail[trie[0][i]]=st
}
while(!q.empty()) {
int u=q.front();q.pop();
for(re int i=0;i<26;++i) {
if(trie[u][i]) {
fail[trie[u][i]]=trie[fail[u]][i];
//從u節點更新其兒子節點的失配指標
//其兒子節點的失配指標指向u節點的失配指標指向的節點的邊權相同的節點
//此處不用一直跳fail指標,因為u節點的fail指標更新先於其兒子的fail指標
q.push(trie[u][i]);
//有這個點,就加入佇列
}
else trie[u][i]=trie[fail[u]][i];
//沒有這個點,就把這個點變為u節點失配指標指向的邊權相同的點
}
}
}
il int ask() {
scanf("%s",str+1);//輸入文字串
int id=0,len=strlen(str+1),res=0;
for(re int i=1;i<=len;++i) {
int to=str[i]-'a';
id=trie[id][to];
for(re int j=id;j&&end[j]!=-1;j=fail[j])
//跳fail指標,看是不是一個模式串的結束節點
res+=end[j],end[j]=-1;
//是就統計答案,並清空防止以後再被訪問到
}
return res;
}
signed main()
{
n=read();
for(re int i=1;i<=n;++i) insert();
get_fail();
printf("%lld\n",ask());
return 0;
}
接下來,我們就可以開始我們的刷題之路了:
P3808 【模板】AC自動機(簡單版)(學習程式碼的題解)(有圖講解的題解)(我的程式碼)
P3796 【模板】AC自動機(加強版)(題解)(我的程式碼)
修改查詢函式,改為記錄模式串出現次數,最後$sort$一遍把出現次數相同的最長的字串輸出即可。
P5357 【模板】AC自動機(二次加強版)(題解)(我的程式碼)
本題要對$fail$陣列進行理解,將其建成一顆$fail$樹,統計$Trie$的終止節點在$fail$樹的子樹上的總匹配次數。
理解:因為$fail$樹的一個節點的子樹的所有節點都可以通過失配指標跳到當前節點,所以他們都有相同的字首(從$trie$樹的根節點到該節點),所以模式串出現次數就是$fail$樹上該節點子樹的文字串出現次數。
P3121 [USACO15FEB]Censoring G(題解)(我的程式碼)
本題要運用棧的思想,分別記錄$AC$自動機掃到的節點和合法的字元,如果$AC$自動機匹配到了就將兩個棧同時彈出匹配到的長度,最後輸出合法字元棧內的字元即可。
本題是$AC$自動機二次加強版的板子,只要改一下模式串就可以得到結果(雙倍經驗)
P3041 [USACO12JAN]Video Game G(題解)(我的程式碼)
本題是$AC$自動機$+ DP$,我們用$dp[i][j]$表示長度為$i$的字串在$Trie$樹上編號為$j$的節點結束的最大得分,再列舉下一個是$ABC$三選一的情況時的匹配次數,找哪個貢獻最大,最後取長度為$k$時的最大值即可。
P4052 [JSOI2007]文字生成器(題解)(我的程式碼)
本題同樣是$AC$自動機$+ DP$,我們用$dp[i][j]$表示長度為$i$且結束節點為$j$的不合法字串數(沒有走到一個字串的結束節點),再運用容斥原理用總數減去長度為$m$的所有不合法字串總數即可。
本題要找無限長的可行串,我們可以轉化一個思路:將$Trie$樹連向兒子節點的邊看做單向邊,再將該節點連向其失配節點的邊看做單向邊,再在$Trie$樹上找環(該環不能經過任意病毒串的結束節點),找的到就有無限長的可行串。(因為可以一直繞著那個環走)
因為以上是兩道一樣的題,所以給一個提供思路的題解,只要處理一下不同的輸入方式即可。
思路:因為要判斷第$x$個字串在第$y$個字串中出現的次數,所以我們可以構造一顆$fail$樹,判斷以$x$的結束節點為根的子樹中,有多少個節點屬於$y$字串(因為如果$y$字串的$fail$指標指向$x$字串,那麼$x$字串一定在$y$字串中出現過(可結合上面的$fail$樹圖進行理解)),對於這個問題,我們可以運用樹鏈剖分的思想,將$fail$樹的每一個節點標記一個$dfs$序,並統計每一個節點的子樹大小,那麼$u$節點子樹對應的區間就是$[dfn[u],dfn[u]+siz[u]-1]$,用樹狀陣列維護即可。
刷題題單
參考資料
《資訊學奧賽一本通提高篇》