AC自动机学习笔记
什么是自動(dòng)機(jī)
一般指確定有限狀態(tài)自動(dòng)機(jī),所以AC自動(dòng)機(jī)不是自動(dòng)AC機(jī)
自動(dòng)機(jī)是一個(gè)非常廣泛使用的數(shù)學(xué)模型
-
自動(dòng)機(jī)是一個(gè)對(duì)信號(hào)序列進(jìn)行判定的模型
解釋一下上面那句話(huà)
信號(hào)序列是指一串有順序的信號(hào)例如字符串的從前到后每一個(gè)字符
判定是指對(duì)某一個(gè)命題給出真或者假的判斷
對(duì)于自動(dòng)機(jī),一共存在3種信號(hào)序列
-
不能識(shí)別
-
判定結(jié)果為真
-
判定結(jié)果為假
-
-
自動(dòng)機(jī)的結(jié)構(gòu)其實(shí)是一張有向圖
其中自動(dòng)機(jī)每個(gè)節(jié)點(diǎn)都是一個(gè)判定節(jié)點(diǎn),節(jié)點(diǎn)只是狀態(tài)而非任務(wù),邊可以接受多種字符
下面的是一個(gè)判斷一個(gè)二進(jìn)制數(shù)是不是偶數(shù)的自動(dòng)機(jī)
從起始結(jié)點(diǎn)開(kāi)始,從高位到低位接受這個(gè)數(shù)的二進(jìn)制序列,看最終停在哪里。
若最終停在紅圈結(jié)點(diǎn),則是偶數(shù),否則則反之
-
自動(dòng)機(jī)只是數(shù)學(xué)模型,不是算法,不是數(shù)據(jù)結(jié)構(gòu)
因此用不同的實(shí)現(xiàn)方法可以得到不同的復(fù)雜度
形式化定義
一個(gè)\(\text{DFA}\)(確定有限狀態(tài)自動(dòng)機(jī),即自動(dòng)機(jī))由五部分組成
-
字符集\(\sum\) :
本自動(dòng)機(jī)只能輸入這些字符
-
狀態(tài)集合\(Q\) :
如果把一個(gè)\(\text{DFA}\)看成有向圖則\(\text{DFA}\)中的狀態(tài)就相當(dāng)于圖上的頂點(diǎn)
-
起始狀態(tài)\(s\) :
對(duì)于\(s\in Q\),\(s\)是一個(gè)特殊的狀態(tài)
-
接受狀態(tài)集合\(F\) :
對(duì)于\(F\subseteq Q\),\(F\)是一組特殊狀態(tài)
-
轉(zhuǎn)移函數(shù)\(\delta\) :
\(\delta\) 是一個(gè)接受兩個(gè)參數(shù)返回一個(gè)值的函數(shù),其中第一個(gè)參數(shù)和返回值都是一個(gè)狀態(tài)而第二個(gè)參數(shù)是字符集\(\sum\)中的一個(gè)字符
把一個(gè)\(\text{DFA}\)看成一張有向圖,\(\text{DFA}\)中的\(\delta\)就相當(dāng)于邊,每條邊上都有一個(gè)字符
\(\text{DFA}\)的作用是識(shí)別字符串,對(duì)于自動(dòng)機(jī) \(\text A\) ,若它能識(shí)別字符串 \(S\) ,那么 \(A(S)=\mathrm{T}\) ,反之\(A(S)=\mathrm{F}\) 。
當(dāng) \(DFA\) 讀入一個(gè)字符串時(shí),從初始狀態(tài)起按照轉(zhuǎn)移函數(shù)一個(gè)一個(gè)字符地轉(zhuǎn)移。
如果讀入完一個(gè)字符串的所有字符后位于一個(gè)接受狀態(tài),那么稱(chēng) \(DFA\) 接受 這個(gè)字符串,反之稱(chēng) \(DFA\) 不接受 這個(gè)字符串。
如狀態(tài) \(v\) 沒(méi)有字符 \(c\) 的轉(zhuǎn)移,則令 \(\delta(v,c)=\mathrm{null}\) ,而 \(\mathrm{null}\) 只能轉(zhuǎn)移到 \(\mathrm{null}\) ,且 \(\mathrm{null}\) 不屬于接受狀態(tài)集合。
無(wú)法轉(zhuǎn)移到接受狀態(tài)的狀態(tài)可以視作 \(\mathrm{null}\) ,也可以說(shuō) \(\mathrm{null}\) 代指所有無(wú)法轉(zhuǎn)移到接受狀態(tài)的狀態(tài)
我們擴(kuò)展定義轉(zhuǎn)移函數(shù) [\delta] ,令其第二個(gè)參數(shù)可以接收一個(gè)字符串: [\delta(v,s)=\delta(\delta(v,s[1]),s[2..|s|])] ,擴(kuò)展后的轉(zhuǎn)移函數(shù)就可以表示從一個(gè)狀態(tài)起接收一個(gè)字符串后轉(zhuǎn)移到的狀態(tài)。那么, [A(s)=[\delta(start,s)\in F]] 。
下圖是一個(gè)接受且僅接受字符串 "\(a\)", "\(ab\)", "\(aac\)" 的 \(\text{DFA}\):
\(\text{AC}\)自動(dòng)機(jī)
AC自動(dòng)機(jī)是以Trie為基礎(chǔ)結(jié)合KMP的思想建立的自動(dòng)機(jī)
KMP算法是求單字符串對(duì)單字符串的匹配使用的,而AC自動(dòng)機(jī)是求多個(gè)字符串在一個(gè)字符串上的匹配使用的
AC自動(dòng)機(jī)的實(shí)現(xiàn)
AC自動(dòng)機(jī)需要提前知道所有的需要匹配的字符串
-
第一步 把需要匹配的字符串構(gòu)建成一棵Trie樹(shù)
-
第二步 對(duì)Trie樹(shù)上的所有節(jié)點(diǎn)構(gòu)造失配指針
構(gòu)建Trie樹(shù)
普通的Trie,該怎么建就怎么建
這里借用大佬的圖片
構(gòu)建失配指針
\(fail\)指針在這里的作用是每次沿著Trie樹(shù)匹配,當(dāng)前位置沒(méi)有匹配上時(shí),直接跳轉(zhuǎn)到失配指針?biāo)赶虻奈恢美^續(xù)進(jìn)行匹配
在這里\(fail\)指針指向的是一個(gè)在\(\text{Trie}\)里存在的最長(zhǎng)的與真后綴相同的字符串。
用OI-wiki的圖舉個(gè)例子
如 \(she\),她的真后綴有 \(he\),\(e\) 和 \(\varnothing\),其中 \(he\) 和 \(\varnothing\) 存在于 \(\text{Trie}\) 樹(shù)中,則讓 \(9\) 號(hào)節(jié)點(diǎn)的 \(fail\) 指針指向最長(zhǎng)的 \(he\) 的末尾節(jié)點(diǎn) \(2\) 號(hào)節(jié)點(diǎn)
如 \(her\),她的真后綴有 \(er\),\(r\) 和 \(\varnothing\),只有 \(\varnothing\) 存在于 Trie 樹(shù)中,則讓 \(3\) 號(hào)節(jié)點(diǎn)的 \(fail\) 指針指向根節(jié)點(diǎn) \(0\)
如何求出失配指針
當(dāng)前節(jié)點(diǎn) \(p\) 代表的字符是 \(c\),\(p\) 的 \(fail\) 指針應(yīng)指向 \(p\) 的父親的 \(fail\) 指針的代表 \(c\) 的兒子
如圖,\(9\) 代表的字符是 \(e\),\(9\) 的父親是 \(8\),\(8\) 的 \(fail\) 指針指向 \(1\),\(1\) 的代表 \(e\) 的兒子是 \(2\),因此 \(9\) 的 \(fail\) 指針指向 \(2\) 號(hào)節(jié)點(diǎn)。
如果\(p\)不存在代表\(c\)的兒子則讓\(c\)的\(fail\)指針指向\(p\)的\(fail\)指針指向的節(jié)點(diǎn)\(p'\)的代表\(c\)的兒子
如OI-wiki上的圖
這里是OI-wiki上的完整動(dòng)圖
- 藍(lán)色結(jié)點(diǎn):BFS 遍歷到的結(jié)點(diǎn) u
- 藍(lán)色的邊:當(dāng)前結(jié)點(diǎn)下,AC 自動(dòng)機(jī)修改字典樹(shù)結(jié)構(gòu)連出的邊。
- 黑色的邊:AC 自動(dòng)機(jī)修改字典樹(shù)結(jié)構(gòu)連出的邊。
- 紅色的邊:當(dāng)前結(jié)點(diǎn)求出的 fail 指針
- 黃色的邊:fail 指針
- 灰色的邊:字典樹(shù)的邊
我們可以以此來(lái)寫(xiě)出代碼
$My\ Code$
#include<bits/stdc++.h>
using namespace std;
namespace IO{
inline void close(){std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0);}
inline void Fire(){freopen(".in","r",stdin);freopen(".out","w",stdout);}
inline int read(){int s = 0,w = 1;char ch = getchar();while(ch<'0'||ch>'9'){ if(ch == '-') w = -1;ch = getchar();}while(ch>='0'&&ch<='9'){ s = s*10+ch-'0';ch = getchar();}return s*w;}
inline void write(int x){char F[200];int tmp=x>0?x:-x,cnt=0;;if(x<0)putchar('-') ;while(tmp>0){F[cnt++]=tmp%10+'0';tmp/=10;}if(cnt==0)putchar('0');while(cnt>0)putchar(F[--cnt]);putchar(' ');}
}
using namespace IO;
class AC{
public:
class Trie{
public:
int fail,vis[26],end;
}Tr[1000000];
int cnt;
inline void clear(){
memset(Tr,0,sizeof(Tr));
}
inline void ins(string s){
int l=s.length(),q=0;
for(int i=0;i<l;++i){
if(!Tr[q].vis[s[i]-'a']) Tr[q].vis[s[i]-'a']=++cnt;
q=Tr[q].vis[s[i]-'a'];
}
Tr[q].end+=1;
}
inline void Get(){
queue<int>Q;
for(int i=0;i<26;++i){
if(Tr[0].vis[i]!=0){
Tr[Tr[0].vis[i]].fail=0;
Q.push(Tr[0].vis[i]);
}
}
while(!Q.empty()){
int u=Q.front();
Q.pop();
for(int i=0;i<26;++i){
if(Tr[u].vis[i]!=0){
Tr[Tr[u].vis[i]].fail=Tr[Tr[u].fail].vis[i];
Q.push(Tr[u].vis[i]);
}
else
Tr[u].vis[i]=Tr[Tr[u].fail].vis[i];
}
}
}
inline int Ask(string s){
int l=s.length(),q=0,ans=0;
for(int i=0;i<l;++i){
q=Tr[q].vis[s[i]-'a'];
for(int t=q;t&&Tr[t].end!=-1;t=Tr[t].fail){
ans+=Tr[t].end;
Tr[t].end=-1;
}
}
return ans;
}
}AC;
signed main(){
// freopen("1.in","r",stdin);
// freopen("1.out","w",stdout);
string s;
int m=read();
while(m--){
int n=read();
AC.clear();
for(int i=1;i<=n;++i){
cin>>s;
AC.ins(s);
}
AC.Get();cin>>s;
write(AC.Ask(s));
puts("");
}
}
洛谷的完整題面
【模板】AC 自動(dòng)機(jī)(簡(jiǎn)單版)
題目描述
給定 \(n\) 個(gè)模式串 \(s_i\) 和一個(gè)文本串 \(t\),求有多少個(gè)不同的模式串在文本串里出現(xiàn)過(guò)。
兩個(gè)模式串不同當(dāng)且僅當(dāng)他們編號(hào)不同。
輸入格式
第一行是一個(gè)整數(shù),表示模式串的個(gè)數(shù) \(n\)。
第 \(2\) 到第 \((n + 1)\) 行,每行一個(gè)字符串,第 \((i + 1)\) 行的字符串表示編號(hào)為 \(i\) 的模式串 \(s_i\)。
最后一行是一個(gè)字符串,表示文本串 \(t\)。
輸出格式
輸出一行一個(gè)整數(shù)表示答案。
樣例 #1
樣例輸入 #1
3
a
aa
aa
aaa
樣例輸出 #1
3
樣例 #2
樣例輸入 #2
4
a
ab
ac
abc
abcd
樣例輸出 #2
3
樣例 #3
樣例輸入 #3
2
a
aa
aa
樣例輸出 #3
2
提示
樣例 1 解釋
\(s_2\) 與 \(s_3\) 編號(hào)(下標(biāo))不同,因此各自對(duì)答案產(chǎn)生了一次貢獻(xiàn)。
樣例 2 解釋
\(s_1\),\(s_2\),\(s_4\) 都在串 abcd 里出現(xiàn)過(guò)。
數(shù)據(jù)規(guī)模與約定
- 對(duì)于 \(50\%\) 的數(shù)據(jù),保證 \(n = 1\)。
- 對(duì)于 \(100\%\) 的數(shù)據(jù),保證 \(1 \leq n \leq 10^6\),\(1 \leq |t| \leq 10^6\),\(1 \leq \sum\limits_{i = 1}^n |s_i| \leq 10^6\)。\(s_i, t\) 中僅包含小寫(xiě)字母。
例題
Keywords Search
AC自動(dòng)機(jī)板子題,注意每組數(shù)據(jù)都需要對(duì)AC自動(dòng)機(jī)進(jìn)行\(clear\)操作
點(diǎn)擊查看代碼
#include<bits/stdc++.h>
using namespace std;
namespace IO{
inline void close(){std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0);}
inline void Fire(){freopen(".in","r",stdin);freopen(".out","w",stdout);}
inline int read(){int s = 0,w = 1;char ch = getchar();while(ch<'0'||ch>'9'){ if(ch == '-') w = -1;ch = getchar();}while(ch>='0'&&ch<='9'){ s = s*10+ch-'0';ch = getchar();}return s*w;}
inline void write(int x){char F[200];int tmp=x>0?x:-x,cnt=0;;if(x<0)putchar('-') ;while(tmp>0){F[cnt++]=tmp%10+'0';tmp/=10;}if(cnt==0)putchar('0');while(cnt>0)putchar(F[--cnt]);putchar(' ');}
}
using namespace IO;
class AC{
public:
class Trie{
public:
int fail,vis[26],end;
}Tr[1000000];
int cnt;
inline void clear(){
memset(Tr,0,sizeof(Tr));
}
inline void ins(string s){
int l=s.length(),q=0;
for(int i=0;i<l;++i){
if(!Tr[q].vis[s[i]-'a']) Tr[q].vis[s[i]-'a']=++cnt;
q=Tr[q].vis[s[i]-'a'];
}
Tr[q].end+=1;
}
inline void Get(){
queue<int>Q;
for(int i=0;i<26;++i){
if(Tr[0].vis[i]!=0){
Tr[Tr[0].vis[i]].fail=0;
Q.push(Tr[0].vis[i]);
}
}
while(!Q.empty()){
int u=Q.front();
Q.pop();
for(int i=0;i<26;++i){
if(Tr[u].vis[i]!=0){
Tr[Tr[u].vis[i]].fail=Tr[Tr[u].fail].vis[i];
Q.push(Tr[u].vis[i]);
}
else
Tr[u].vis[i]=Tr[Tr[u].fail].vis[i];
}
}
}
inline int Ask(string s){
int l=s.length(),q=0,ans=0;
for(int i=0;i<l;++i){
q=Tr[q].vis[s[i]-'a'];
for(int t=q;t&&Tr[t].end!=-1;t=Tr[t].fail){
ans+=Tr[t].end;
Tr[t].end=-1;
}
}
return ans;
}
}AC;
signed main(){
// freopen("1.in","r",stdin);
// freopen("1.out","w",stdout);
string s;
int m=read();
while(m--){
int n=read();
AC.clear();
for(int i=1;i<=n;++i){
cin>>s;
AC.ins(s);
}
AC.Get();cin>>s;
write(AC.Ask(s));
puts("");
}
}
下面是花絮
總結(jié)
- 上一篇: Vite4+Typescript+Vue
- 下一篇: 从零开始用 Axios 请求后端接口