AC自动机:初步理解可以理解为就是KMP加上trie AC自动机讲解超详细 - Hastieyua - 博客园 (cnblogs.com)
这个是模板:187 AC自动机 - 董晓 - 博客园 (cnblogs.com) 求是否出现过,只要匹配到就清0,不管重复的
https://www.bilibili.com/video/BV1tF41157Dy/?vd_source=23dc8e19d485a6ac47f03f6520fb15c2 董老师讲解的视频
重点就是:
ne[v]存的是节点v的回跳边的终点(四边形),ch[u][i]存的是节点u的树边或者转移边(三角形)
回跳边所指的是当前节点父节点回跳边指向节点的儿子
转移边所指的是当前节点的回跳边所指结点的儿子


和KMP的对比:

P3796 【模板】AC 自动机(加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
有 N个由小写字母组成的模式串以及一个文本串 T。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串 T 中出现的次数最多。保证没有两个模式串重复
这里就需要处理重复出现次数
//AC自动机加强版
#include<bits/stdc++.h>
#define maxn 1000001
using namespace std;
char s[151][maxn],T[maxn];
int n,cnt,vis[maxn],ans;
struct kkk{
int son[26],fail,flag;
void clear(){memset(son,0,sizeof(son));fail=flag=0;}
}trie[maxn];
queue<int>q;
void insert(char* s,int num){
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int v=s[i]-'a';
if(!trie[u].son[v])trie[u].son[v]=++cnt;
u=trie[u].son[v];
}
trie[u].flag=num; //变化1:标记为第num个出现的字符串
}
void getFail(){
for(int i=0;i<26;i++)trie[0].son[i]=1;
q.push(1);trie[1].fail=0;
while(!q.empty()){
int u=q.front();q.pop();
int Fail=trie[u].fail;
for(int i=0;i<26;i++){
int v=trie[u].son[i];
if(!v){trie[u].son[i]=trie[Fail].son[i];continue;}
trie[v].fail=trie[Fail].son[i];
q.push(v);
}
}
}
void query(char* s){
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int v=s[i]-'a';
int k=trie[u].son[v];
while(k>1){
if(trie[k].flag)vis[trie[k].flag]++; //如果有模式串标记,更新出现次数
k=trie[k].fail;
}
u=trie[u].son[v];
}
}
void clear(){
for(int i=0;i<=cnt;i++)trie[i].clear();
for(int i=1;i<=n;i++)vis[i]=0;
cnt=1;ans=0;
}
int main(){
while(1){
scanf("%d",&n);if(!n)break;
clear();
for(int i=1;i<=n;i++){
scanf("%s",s[i]);
insert(s[i],i);
}
scanf("%s",T);
getFail();
query(T);
for(int i=1;i<=n;i++)ans=max(vis[i],ans); //最后统计答案
printf("%d\n",ans);
for(int i=1;i<=n;i++)
if(vis[i]==ans)
printf("%s\n",s[i]);
}
}
P5357 【模板】AC 自动机(二次加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
求出每个模式串在串中的出现次数,注意这里两个模式串可能是重复的
可以用两种方法做,一种是从底向上,用拓扑排序,第二种是之间建fail树,然后求子树和,这两种方法都可以,重点是理解fail边变为树,以及怎样通过这种方法降低复杂度,避免重复跳fail边,导致超时
1、拓扑排序
#include <queue>
#include <cstdlib>
#include <cmath>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 2e6+100;
int n,cnt,mp[maxn],in[maxn],ans,vis[maxn];
char s[maxn],t[maxn];
struct node{
int son[26],flag,ans,fail;
void clea(){
memset(son,0,sizeof(son));fail=flag=ans=0;
}
}trie[maxn];
queue<int> q;
void inser(char *s,int num){
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int v=s[i]-'a';
if(!trie[u].son[v]) trie[u].son[v]=++cnt;
u=trie[u].son[v];
}
if(!trie[u].flag) trie[u].flag=num;
mp[num]=trie[u].flag; //注意这里是在处理重复子串
//这道题有相同字符串要统计,设当前字符串是第i个,我们用一个Map[i]数组(不是STL那个)存((当前字符串在Trie中的那个位置)的flag),
//最后把vis[Map[i]]输出就OK了。另外flag只在第一次赋值时变化,其他都不变。
}
void get_fa(){
for(int i=0;i<26;i++) trie[0].son[i]=1;
q.push(1);
while(!q.empty()){
int u=q.front();
q.pop();
int Fail=trie[u].fail;
for(int i=0;i<26;i++){
int v=trie[u].son[i];
if(!v){
trie[u].son[i]=trie[Fail].son[i];continue;
}
trie[v].fail=trie[Fail].son[i];
in[trie[v].fail]++; //从底到根算:拓扑排序
q.push(v);
}
}
}
void tuop(){
for(int i=1;i<=cnt;i++) if(in[i]==0) q.push(i);
while(!q.empty()){
int u=q.front();
q.pop();
vis[trie[u].flag]=trie[u].ans;
int v=trie[u].fail;
in[v]--;
trie[v].ans+=trie[u].ans; //向上传递值
//这里是拓扑排序了,如果入度为0了,也就是说他的儿子都算完了,就可以入队了
if(in[v]==0) q.push(v);
}
}
void query(char *s){
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
u=trie[u].son[s[i]-'a'];
trie[u].ans++;
}
}
int main() {
scanf("%d",&n);
cnt=1;
for(int i=1;i<=n;i++){
scanf("%s",s);inser(s,i);
}
get_fa();
scanf("%s",t);
query(t);
tuop();
for(int i=1;i<=n;i++) printf("%d\n",vis[mp[i]]);
return 0;
}
2、建fail树统计子树和
#include <cstdio>
#include <iostream>
#include <algorithm>
#include<queue>
#define base 139
const int maxn=2e5+10;
using namespace std;
const int maxm=2e6+10;
typedef unsigned long long ll;
char s[maxm];
queue<int> q;
int head[maxn],nex[maxn],to[maxn],cnt;
int n,tr[maxn][26],fail[maxn],match[maxn],size[maxn];
int tot=1;
void dfs(int u){
for(int i=head[u];i;i=nex[i]){
int v=to[i];
dfs(v);
size[u]+=size[v];
}
}
void add(int u,int v){//建边
nex[++cnt]=head[u];
to[cnt]=v;
head[u]=cnt;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s);
int u=1;
for(int j=0;s[j];j++){
int c=s[j]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
match[i]=u; //记录每个模式串在 Trie 树上的终止节点
}
for(int i=0;i<26;i++) tr[0][i]=1;
q.push(1);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q.push(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
scanf("%s",s);
for(int u=1,i=0;s[i];i++){
u=tr[u][s[i]-'a'];
++size[u]; //记录匹配次数
}
for(int i=2;i<=tot;i++) add(fail[i],i); //建fail树
dfs(1);
for(int i=1;i<=n;i++) printf("%d\n",size[match[i]]);
return 0;
}
1479:【例题1】Keywords Search
这个也是模板题
注意开的数据范围,重点是看getfail函数和que函数的写法(记住呀)
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e4+10;
const int M=1e6+10;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//AC自动机模板题
char s[M];
int ch[maxn*32][26];
int fail[maxn*32],flag[maxn*32];
int t,n,tot=0;
void inti(){
memset(ch,0,sizeof(ch));
memset(flag,0,sizeof(flag));
memset(fail,0,sizeof(fail));
tot=0;
}
void inse(string s){
int now=0;
for(int i=0;i<s.length();i++){
int x=s[i]-'a';
if(!ch[now][x]){
ch[now][x]=++tot;
}
now=ch[now][x];
}
flag[now]++; //这里单词数+1
}
void getfail(){
queue<int> q;
for(int i=0;i<26;i++){
if(ch[0][i]) {
fail[ch[0][i]]=0;
q.push(ch[0][i]);
}
}
while(!q.empty()){
int op=q.front();
q.pop();
for(int i=0;i<26;i++){
if(ch[op][i]){
fail[ch[op][i]]=ch[fail[op]][i];
q.push(ch[op][i]);
}
else ch[op][i]=ch[fail[op]][i];
}
}
}
int que(string s){
int now=0,ans=0;
for(int i=0;i<s.size();i++){
now=ch[now][s[i]-'a'];
for(int j=now;j&&flag[j]!=-1;j=fail[j]){
ans+=flag[j];
flag[j]=-1;
}
}
return ans;
}
int main(){
scanf("%d",&t);
while(t--){
inti();
scanf("%d",&n);
string tmp;
for(int i=0;i<n;i++){
cin>>tmp;
inse(tmp);
}
fail[0]=0;
getfail();
scanf("%s",s);
printf("%d\n",que(s));
}
return 0;
}
1480:玄武密码

重点还是理解数据结构啊。。。
我们只需要先建立所有密码的trie树
再以母串为主串跑一个AC自动机
不过其中还是有一些需要改动的地方
原本字典树中用来记录某个节点是不是字符串结尾的数组不需要,直接删去
我们需要另一个数组来标记哪些点被匹配
跑完ac自动机后从trie树上找最后一个匹配的点即可
优化:由于nxt数组是递归到0的所以只要有一个点被标记过,那么这个点到0的所有点都已经被遍历过直接退出即可
//为什么一个点超时啊。。。。
有的说是SAM好做一些,还不会先留着
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e7+10;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
/*
我们只需要先建立所有密码的trie树
再以母串为主串跑一个AC自动机
不过其中还是有一些需要改动的地方
原本字典树中用来记录某个节点是不是字符串结尾的数组不需要,直接删去
我们需要另一个数组来标记哪些点被匹配
跑完ac自动机后从trie树上找最后一个匹配的点即可
优化:由于nxt数组是递归到0的所以只要有一个点被标记过,那么这个点到0的所有点都已经被遍历过直接退出即可
*/
//为什么一个点超时啊。。。。
int ch[maxn][4];
int fail[maxn];
int flag[maxn];
int n,m,tot=0;
char ss[maxn];
char sa[100050][150];
int jud(char x){
if(x=='E') return 0;
else if(x=='S') return 1;
else if(x=='W') return 2;
else if(x=='N') return 3;
}
void inse(string s){
int now=0;
for(int i=0;i<s.length();i++){
int x=jud(s[i]);
if(!ch[now][x]){
ch[now][x]=++tot;
}
now=ch[now][x];
} //flag不用加了,因为是用来判断有没有访问过的
}
void getfail(){
queue<int> q;
for(int i=0;i<4;i++) {
if(ch[0][i]) q.push(ch[0][i]);
}
while(!q.empty()){
int u=q.front();q.pop();
for(int i=0;i<4;i++){
if(ch[u][i]) fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]);
else ch[u][i]=ch[fail[u]][i];
}
}
}
void biaoji(string s){
int u=0;
for(int i=0;i<s.length();i++){
int c=jud(s[i]);
u=ch[u][c];
for(int j=u;j;j=fail[j]){
if(flag[j]) break;
flag[j]=1;
}
}
}
int getan(string s){
int u=0,ans=0;
for(int i=0;i<s.length();i++){
int c=jud(s[i]);
u=ch[u][c];
if(flag[u]) ans=i+1; //这里仔细看,求的是最长的匹配值,如果不匹配了就不会改变这个ans了,所以ans里面放的是最大的
}
return ans;
}
int main(){
scanf("%d %d",&n,&m);
scanf("%s",ss);
for(int i=0;i<m;i++){
scanf("%s",sa[i]);
inse(sa[i]);
}
getfail();
biaoji(ss);
for(int i=0;i<m;i++){
printf("%d\n",getan(sa[i]));
}
return 0;
}
1481:Censoring

就是给一个原串,给出n个模式串,匹配后删掉,注意事项是删掉一个后可能又能够匹配另一个串了
和KMP里面的一道题一样,这里能够通的过了...
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
char s[100001],ori[100001];
int n,tot,w,top;
int trie[100001][26],fail[100001],heap[100001],sign[100001];
int isend[100001];
void insert(char *s){
int now=0,len=strlen(s);
for(int i=0;i<len;i++){
int x=s[i]-'a';
if(!trie[now][x])trie[now][x]=++tot;
now=trie[now][x];
}
isend[now]=len;
}
void makefail(){
queue<int> q;
for(int i=0;i<26;i++)
if(trie[0][i])q.push(trie[0][i]);
while(!q.empty()){
int now=q.front();q.pop();
for(int i=0;i<26;i++){
if(!trie[now][i]){
trie[now][i]=trie[fail[now]][i];
continue;
}
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
}
}
}
void solve(char *s){
int now=0,len=strlen(s),i=0;
w=0;
while(i<len){
int x=s[i]-'a';
now=trie[now][x];
sign[++top]=now;
heap[top]=i;
if(isend[now]){
top-=isend[now];
if(!top) now=0;
else now=sign[top];
}
i++;
}
}
int main()
{
scanf("%s",s);
scanf("%d",&n);
int len=strlen(s);
for(int i=1;i<=n;i++){
scanf("%s",ori);
insert(ori);
}
makefail();
solve(s);
for(int i=1;i<=top;i++)
printf("%c",s[heap[i]]);
return 0;
}
1482:单词

由模式串组成的原串
把所有字符串放在Trie里,并记cnt[i]为Trie的节点i为多少个字符串的前缀。
一个字符串是另一个字符串的子串,那么它也是该字符串某个前缀s[0,m]的后缀。
那么一个想法就出来了:
求出fail数组,然后以fail指针为边建出fail树,那么一个字符串的出现个数为:设它的结尾是节点x,那么fail树上以x为根的子树的cnt值的总和即为答案。
原文链接:https://blog.csdn.net/worldwide_d/article/details/51819862
//https://blog.csdn.net/worldwide_d/article/details/51819862
这道题也可以直接用AC自动机做,就是自己构建原串,在模式串中间加一个字符例如%
/*
把所有字符串放在Trie里,并记cnt[i]为Trie的节点i为多少个字符串的前缀。
一个字符串是另一个字符串的子串,那么它也是该字符串某个前缀s[0,m]的后缀。
那么一个想法就出来了:
求出fail数组,然后以fail指针为边建出fail树,那么一个字符串的出现个数为:设它的结尾是节点x,那么fail树上以x为根的子树的cnt值的总和即为答案。
原文链接:https://blog.csdn.net/worldwide_d/article/details/51819862
//https://blog.csdn.net/worldwide_d/article/details/51819862
*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
const int maxn=1e6+10;
using namespace std;
int n,cnt,ch[maxn][26],size[maxn],fail[maxn];
int mp[maxn],h[maxn];
char s[maxn];
struct node{
void ins(int x){
scanf("%s",s+1);
int now=0,len=strlen(s+1);
for(int i=1;i<=len;i++){
int u=s[i]-'a';
if(!ch[now][u]) ch[now][u]=++cnt;
now=ch[now][u];
size[now]++; //
}
mp[x]=now; //标记这个模式串在树上的位置
}
void build(){
// queue<int> q;
// for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
// while(!q.empty()){
// int u=q.front();q.pop();
// for(int i=0;i<26;i++){
// if(tr[u][i]) {
// fail[tr[u][i]]=tr[fail[u]][i];
// }
// else ch[u][i]=tr[fail[u]][i];
// }
// }
//这里用手写队列
int head=0,tail=0;
for(int i=0;i<26;i++) if(tr[0][i]) h[++tail]=ch[0][i];
while(head<tail){
int x=h[++head];
for(int i=0;i<26;i++){
int y=tr[x][i];
if(y) {
fail[y]=tr[fail[x]][i];
h[++tail]=y;
}
else ch[x][i]=tr[fail[x]][i];
}
}
}
void solve(){
for(int i=cnt;i>=0;i--) size[fail[f[i]]]+=size[h[i]];
for(int i=1;i<=n;i++) printf("%d\n",size[a[i]]);
}
}ac;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) ac.ins(i);
ac.build();
ac.solve();
return 0;
}
1483:最短母串

【题解】
类似于搜索+二进制记录状态的题目
搜索时利用BFS来跑,每一个结点的位置都可以用状态数组存起来,
判断是否为 (1<<n)- 1 即可。
在输出答案时需要递归实现,所以要一个辅助数组fa来记录上一个结点的位置。
洛谷 P2322 最短母串问题 状压+AC自动机_二货RK的博客-CSDN博客
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxn=6600;
const int maxm=2e6+200;
int tr[maxn][26],fail[maxn],end[maxn]; //这里END就是标记节点属于哪一条字符串的
int vis[maxn][605]; //这里是节点--状态
int fa[maxm],q[maxm],st[maxm];
//fa是为了标记上一个节点属于哪一个
//这个例子是可以模拟了,勉勉强强理解了
//?????编译错误
char str[maxm];
char mm[660];
int n,cnt;
void prin(int x){
if(x==1) return;
prin(fa[x]);
printf("%c",str[x]+'A');
}
void inse(char s[],int id){
int p=0;
for(int i=0;s[i];i++){
int x=s[i]-'A';
if(!tr[p][x]){
tr[p][x]=++cnt;
}
p=tr[p][x];
}
end[p]|=(1<<id); //结尾打上标记 使用的数位思想
}
void build(){
int head=1,tail=0;
for(int i=0;i<26;i++){
if(tr[0][i]){
q[++tail]=tr[0][i];
fail[tr[0][i]]=0;
}
}
while(head<=tail){
int u=q[head++];
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q[++tail]=tr[u][i];
end[tr[u][i]]|=end[fail[tr[u][i]]]; //这里也打上标记
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
void solve(){
memset(q,0,sizeof(q));
int head=0,tail=1;
q[1]=st[1]=0; //节点队列和状态队列
vis[0][0]=1; //预处理
while(head<tail){
int u=q[++head];
int s=st[head];
for(int i=0;i<26;i++){
int to=tr[u][i];
int ts=s|end[to];
if(vis[ts][to]) continue; //避免重复计算
fa[++tail]=head;
q[tail]=to;
str[tail]=i; //记录一下字符串
vis[ts][to]=1;
st[tail]=ts;
if(ts==(1<<n)-1) {
prin(tail);
printf("\n");return;
}
}
}
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%s",mm);
inse(mm,i);
}
build();
solve();
return 0;
}
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 6e3+5;
const int M = 2e6+50;
const int Str_N = 605;
int Trie[N][26],fail[N],End[N];
int vis[N][Str_N];
int Q[M],St[M];
int Fa[M];
char str[M];
int Head,Tail;
int n,idx;
char Str[Str_N];
void print(int x){
if(x==1) return ;
print(Fa[x]);
putchar(str[x]+'A');
}
void Insert( char s[] , int Id ){
int p = 0 ;
for(int i=0;s[i];i++){
int t = s[i]-'A';
if( !Trie[p][t] )
Trie[p][t] = ++idx;
p = Trie[p][t];
}
End[p] |= (1<<Id);
//cout<<p<<" "<<End[p]<<endl;
}
void Build(){
Head = 1 , Tail = 0;
for(int i=0;i<26;i++){
if( Trie[0][i] ){
Q[++Tail] = Trie[0][i];
fail[Trie[0][i]] = 0;
}
}
//cout<<"now"<<endl;
while( Head <= Tail ){
int u = Q[Head];
for(int i=0;i<26;i++){
int To = Trie[u][i];
if( To ){
fail[To] = Trie[fail[u]][i];
Q[++Tail] = To ;
End[To] |= End[fail[To]];
}else{
Trie[u][i] = Trie[fail[u]][i];
}
}
//cout<<Q[Head]<<" "<<fail[Q[Head]]<<" "<<End[Q[Head]]<<endl;
Head++;
}
}
void Solve(){
memset(Q,0,sizeof Q );
Head = 0 , Tail = 1;
Q[1] = St[1] = 0 ;
vis[0][0] = 1 ;
//cout<<"new"<<endl;
while( Head < Tail ){
int u = Q[++Head],S = St[Head];
for(int i=0;i<26;i++){
int To = Trie[u][i];
int Ts = S | End[To] ;
if( vis[Ts][To] ) continue;
Fa[++Tail] = Head ; Q[Tail] = To ;str[Tail] = i;
vis[Ts][To] = 1 ;St[Tail] = Ts ;
/// cout<<To<<" "<<Ts<<" "<<Tail<<" "<<Fa[Tail]<<endl;
if( Ts == (1<<n)-1 ){
print(Tail);
putchar('\n');
return ;
}
}
}
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%s",Str);
Insert(Str,i);
}
Build();
//cout<<endl<<endl;
//for(int i=0;i<26;i++) cout<<Trie[4][i]<<" ";
// cout<<endl<<endl;
Solve();
return 0;
}
/*
96 *
97 4
98 HNOI
99 NOIP
100 NOI
101 IOI
102
103 HNOIPIOI
104 */
posted on
浙公网安备 33010602011771号