2024.3.21 【少年与爱永不老去,即便披荆斩棘,丢失怒马鲜衣。】
Thursday 二月十二
AC自动机
前置芝士:
相关例题:
AC自动机适用于字符串匹配等问题的求解,是一个Tie和KMP思想的结合体
基础的 Trie 结构,将所有的模式串构成一棵 Trie。
KMP 的思想,对 Trie 树上所有的结点构造失配指针。
不难发现,AC自动机还是比较简朴的。
失配指针是AC自动机中十分重要的一点,也是自动机构建的难点和要点,所以先看题再讲。
P3808 AC 自动机(简单版)
//2024.3.21
//by white_ice
#include<bits/stdc++.h>
using namespace std;
#define itn int
//#define fore(x) cin>>x&fo(int i=1;i<=(x);i++)
namespace triee{
const int oo = 1000006;
struct nod{
itn fail;
itn son[26],end;
}trie[oo];
itn cnt = 0;
void insert(string s){
itn l = s.length();
itn now = 0;
for (int i=0;i<l;i++){
if (trie[now].son[s[i]-'a']==0)
trie[now].son[s[i]-'a'] = ++cnt;
now = trie[now].son[s[i]-'a'];
}
trie[now].end+=1;
}
void get(){
queue<int> q;
for (itn i=0;i<26;i++){
if (trie[0].son[i]!=0){
trie[trie[0].son[i]].fail = 0;
q.push(trie[0].son[i]);
}
}
while (!q.empty()){
int u = q.front();
q.pop();
for (itn i=0;i<26;i++){
if (trie[u].son[i]!=0){
trie[trie[u].son[i]].fail = trie[trie[u].fail].son[i];
q.push(trie[u].son[i]);
}
else trie[u].son[i] = trie[trie[u].fail].son[i];
}
}
}
itn find(string s){
int l = s.length();
int now=0,ans=0;
for(int i=0;i<l;++i){
now=trie[now].son[s[i]-'a'];
for(int t=now;t&&trie[t].end!=-1;t=trie[t].fail){
ans+=trie[t].end;
trie[t].end=-1;
}
}
return ans;
}
}
int n;
string s;
int main(){
using namespace triee;
cin >> n;
for(itn i=1;i<=n;i++){
cin >>s;
triee::insert(s);
}
triee::trie[0].fail = 0;
triee::get();
cin >> s;
cout << triee::find(s) ;
return 0 ;
}
在本代码中使用class封装了一个完整的AC自动机,trie树的结构体中,有一个名为fail的指针,他就是失配指针。
fail指针由一个状态指向另一个状态,即状态甲是状态乙的最长后缀,注意后缀是最长的,若干个中最长的。AC自动机中,同一字符串可以匹配多个指针。
下面是fail指针的构建
考虑字典树中当前的结点 u,u 的父结点是 p,p 通过字符 c 的边指向 u,即 t r i e [ p , c ] = u trie[p,\mathtt{c}]=u trie[p,c]=u。假设深度小于 u 的所有结点的 fail 指针都已求得。
- 如果$ \text{trie}[\text{fail}[p],\mathtt{c}] $存在:则让 u 的 fail 指针指向 trie [ fail [ p ] , c ] \text{trie}[\text{fail}[p],\mathtt{c}] trie[fail[p],c]。相当于在 p 和$ \text{fail}[p]$ 后面加一个字符 c,分别对应 u 和 f a i l [ u ] fail[u] fail[u]。
- 如果 trie [ fail [ p ] , c ] \text{trie}[\text{fail}[p],\mathtt{c}] trie[fail[p],c] 不存在:那么我们继续找到$ \text{trie}[\text{fail}[\text{fail}[p]],\mathtt{c}]$。重复 1 的判断过程,一直跳 fail 指针直到根结点。
- 如果真的没有,就让 fail 指针指向根结点。
以上讲的还是非常清楚的啊(因为不是我讲),我只指出几个小点完善,
- fail指针记得要有指向目标,比如根
- 注意最长后缀,最长
- 使用时记得一定要调用初始化函数!
P3796 AC 自动机(简单版 II)
//2024.3.21
//by white_ice
#include<bits/stdc++.h>
using namespace std;
#define itn int
#define fore(x) for(int i=1;i<=x;i++)
itn n;
struct result{itn num,pos;}st[1000006];
bool operator <(result a,result b){return a.num==b.num?a.pos<b.pos:a.num>b.num;}
//namespace AC{
const int oo = 1000006;
string s[oo];
struct triee{
itn fail;
itn son[26],end;
}trie[oo];
int cnt = 0;
void clean(itn x){
memset(trie[x].son,0,sizeof(trie[x].son));
trie[x].fail = trie[x].end = 0;
}
void init(){cnt = 0,clean(0);}
void insert(string s,itn num){
itn l = s.length();
itn now = 0;
for (int i=0;i<l;i++){
if (trie[now].son[s[i]-'a']==0){
trie[now].son[s[i]-'a'] = ++ cnt;
clean(cnt);
}
now = trie[now].son[s[i]-'a'];
}
trie[now].end = num;
}
void get(){
queue<int> q;
for(int i=0;i<26;++i){
if(trie[0].son[i]!=0){
trie[trie[0].son[i]].fail=0;
q.push(trie[0].son[i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;++i){
if(trie[u].son[i]!=0){
trie[trie[u].son[i]].fail=trie[trie[u].fail].son[i];
q.push(trie[u].son[i]);
}
else trie[u].son[i]=trie[trie[u].fail].son[i];
}
}
}
itn find(string s){
itn l = s.length();
itn now = 0,ans= 0;
for (int i=0;i<l;i++){
now = trie[now].son[s[i]-'a'];
for (itn t=now;t;t=trie[t].fail)
st[trie[t].end].num++;
}
return ans;
}
//}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
//using namespace AC;
while (cin>>n&&n){
init();
for (itn i=1;i<=n;i++){
cin >> s[i];
st[i].num = 0;
st[i].pos = i;
insert(s[i],i);
}
trie[0].fail = 0;
get();
cin >> s[0];
find(s[0]);
sort(st+1,st+n+1);
cout << st[1].num <<endl<<s[st[1].pos]<<endl;
for (itn i=2;i<=n;i++){
if (st[i-1].num==st[i].num)
cout << s[st[i].pos]<<endl;
else break;
}
}
return 0;
}