AC自动机

背景

给出一个字典,和若干询问:多少个字典串在询问串中出现过。
即单串与多串的匹配问题。

AC自动机

AC 自动机基于 Trie,将 KMP 的 Border 概念推广到多模式串上。
AC 自动机是一种离线型数据结构,即不支持增量添加新的字符串。
AC 自动机常用于将字符串询问类的问题进行离线处理,也经常与各种 DP 结合,或是补全成 Trie 图。

border概念的推广

推广到两个串:对于两个串 S 和 T,相等的 p 长度的 S 的后缀和 T 的
前缀称为一个 border。
推广到一个字典:对于串 S 和一个字典 D,相等的 p 长度的 S 的后缀,
和任意一个字典串 T 的前缀称为一个 border。
失配(Fail)指针: 对于 Trie 中的每一个节点(即某个字典串的前缀),
它与 Trie 中所有串的最大 Border 即为失配指针。

失配指针

类似与 KMP 求 Border,任意节点的 Border 长度减一,一定是父节点的
Border。因此可以通过遍历父节点的失配指针链来求解。
因此在求失配指针的时候,一定要按长度从小到大来求,即 bfs。

image

image
image
image

image

复杂度分析

类似于 KMP 的势能分析方法,势能总量等于 Trie 的节点总数,因此复
杂度为线性的。

模板

点击查看代码
struct ACAM{
	int tot,ch[maxn][27],fail[maxn],biao[maxn];
	
	void insert(string s){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'A';
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		biao[u]=1;
		return ;
	} 
	
	void getfail(){
		queue<int>q;
		for(int i=0;i<26;i++)if(ch[0][i])q.push(ch[0][i]);
		while(!q.empty()){
			int u=q.front();q.pop();
			for(int i=0;i<26;i++){
				if(ch[u][i]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
                                        //biao[ch[u][i]]|=biao[fail[ch[u][i]]];
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	
}A;

例题

1.【模板】AC 自动机(二次加强版)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-8;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int ans[maxn],id[maxn],cnt[maxn],in[maxn];
struct ACAM{
	int tot,ch[maxn][27],biao[maxn],fail[maxn];
	void insert(string s,int pos){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'a';
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		id[pos]=u;
		return ;
	} 
	
	void getfail(){
		queue<int>q;
		for(int i=0;i<26;i++)if(ch[0][i])q.push(ch[0][i]);
		while(!q.empty()){
			int u=q.front();q.pop();
			for(int i=0;i<26;i++){
				if(ch[u][i]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
					in[fail[ch[u][i]]]++;
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	
	void find(string s){
		int u=0,lenth=s.length();
		for(int i=0;i<lenth;i++){
			int k=s[i]-'a';
			u=ch[u][k];
            int tmp=u;
            while(tmp){
                cnt[tmp]++;
                tmp=fail[tmp];
            }
		}
		return ;
	}
	
	void get_ans(){
		queue<int>q;
		for(int i=1;i<=tot;i++)if(!in[i])q.push(i);
		while(!q.empty()){
			int u=q.front();q.pop();
			int v=fail[u];
			cnt[v]+=cnt[u];
			in[v]--;
			if(!in[v])q.push(v);
		}
		return ;
	}
}A;
int main(){
	int n=read();
	for(int i=1;i<=n;i++){
		string s;cin>>s;
		A.insert(s,i);
	}
	
	A.getfail();
	string now;cin>>now;
	A.find(now);//A.get_ans();
	for(int i=1;i<=n;i++)cout<<cnt[id[i]]<<endl;
    return 0;

}

2.string
题意:先输入n个字符串放在仓库中,然后m次询问,每一次询问有两种类型。类型1是在仓库中新添加一个字符串。类型2是输入一个字符串,然后问当前仓库中有多少字符串是你操作2输入的字符串的子串。
题解:
AC自动机无法在线处理这道题,考虑离线
我们首先将所有在仓库的字符串(包括初始化的n个和之后所有类型1的询问)构建AC自动机
然后构建fail树
若fail树上一个特殊点(字符串的结尾)能够被匹配到,那么这个点到根的路径上的所有特殊点都可以被匹配到
那么我们可以考虑dfs序+树状数组来处理,处理出每个特殊点到根的特殊点数:每个特殊点在它的子树内节点权值+1

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-8;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int limi,f[maxn];
int lowbit(int x){return x&(-x);}
void add(int pos,int val){
	for(int i=pos;i<=limi;i+=lowbit(i))f[i]+=val;
}
int query(int pos){
	int ans=0;
	for(int i=pos;i;i-=lowbit(i))ans+=f[i];
	return ans;
}
int id[maxn]; //记录特殊点的位置 
struct ACAM{
	int tot,ch[maxn][27],fail[maxn];
	int top,l[maxn],r[maxn];
	void init(){
		top=0;
		memset(l,0,sizeof(int)*(tot+1));
		memset(r,0,sizeof(int)*(tot+1));
		for(int i=0;i<=tot;i++)for(int j=0;j<26;j++)ch[i][j]=0;
		memset(fail,0,sizeof(int)*(tot+1));
		memset(f,0,sizeof(int)*(tot+10));
		tot=0;
		return;
	}
	void insert(string s,int pos){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'a';
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		id[pos]=u;
		return ;
	} 
	
	void getfail(){
		queue<int>q;
		for(int i=0;i<26;i++)if(ch[0][i])q.push(ch[0][i]);
		while(!q.empty()){
			int u=q.front();q.pop();
			for(int i=0;i<26;i++){
				if(ch[u][i]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	void solve(){
		vector<vector<int> >G(tot+1);
		for(int i=1;i<=tot;i++)G[fail[i]].pb(i);
		function<void(int)> dfs=[&](int u){
			l[u]=++top;
			for(auto v:G[u])dfs(v);
			r[u]=top;
		};
		dfs(0); 
		return ;
	}
	
	int find(string s){
		int u=0,lenth=s.length(),ans=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'a';
			u=ch[u][k];
            ans+=query(l[u]);
		}
		return ans;
	}

}A;
struct que{
	int opt;
	string s;
};
void solve(){
	int n=read(),m=read();
	for(int i=1;i<=n;i++){
		string s;cin>>s;
		A.insert(s,i);
	}
	vector<que>q(m+1);
	for(int i=1;i<=m;i++){
		q[i].opt=read();cin>>q[i].s;
		if(q[i].opt==1)A.insert(q[i].s,i+n);
	}
	A.getfail();
	A.solve();
	limi=A.tot+2;
	for(int i=1;i<=n;i++){
		add(A.l[id[i]],1);add(A.r[id[i]]+1,-1); 
	}
	for(int i=1;i<=m;i++){
		if(q[i].opt==1){
			add(A.l[id[i+n]],1);add(A.r[id[i+n]]+1,-1);
		}
		else printf("%d\n",A.find(q[i].s));
	}
	A.init();
	return;
}
int main(){
	int t=read();
	while(t--)solve();	
    return 0;

}

3.[CERC2018]The ABCD Murderer

题解:

考虑如何得到最长的\(S_i\)
我们可以在求fail的时候,维护每个AC自动机上点的最长后缀长度的biao数组

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+101;
const int MOD=998244353;
const int inf=2147483647-2;
const double pi=acos(-1);
const double eps=1e-8;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
struct SgT{
	int tr[maxn];
	void build(int k,int l,int r){
		if(l==r){
			tr[k]=inf;
			return ;
		}
		int mid=(l+r)>>1;
		build(k<<1,l,mid);build(k<<1|1,mid+1,r);
		tr[k]=min(tr[k<<1],tr[k<<1|1]);
		return ;
	}
	
	void modify(int k,int l,int r,int pos,int val){
		if(pos<l || r<pos)return ;
		if(l==r){
			tr[k]=val;
			return ; 
		}
		int mid=(l+r)>>1;
		modify(k<<1,l,mid,pos,val);modify(k<<1|1,mid+1,r,pos,val);
		tr[k]=min(tr[k<<1],tr[k<<1|1]);
		return ;
	}
	
	int get_minn(int k,int l,int r,int L,int R){
		if(r<L || R<l)return inf;
		if(L<=l && r<=R)return tr[k];
		int mid=(l+r)>>1;
		return min(get_minn(k<<1,l,mid,L,R),get_minn(k<<1|1,mid+1,r,L,R));
	}
}T;
struct ACAM{
	int tot,ch[maxn][27],fail[maxn],biao[maxn],id[maxn];
	
	void insert(string s){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'a';
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		id[u]=1;biao[u]=lenth; 
		return ;
	} 
	
	void getfail(){
		queue<int>q;
		for(int i=0;i<26;i++)if(ch[0][i])q.push(ch[0][i]);
		while(!q.empty()){
			int u=q.front();q.pop();
			for(int i=0;i<26;i++){
				if(ch[u][i]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
					if(biao[fail[ch[u][i]]]){
						//按照bfs顺序,biao[fail[ch[u][i]]一定先求出来了 
						biao[ch[u][i]]=max(biao[ch[u][i]],biao[fail[ch[u][i]]]);
					}
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	int dp[maxn];
	int find(string s){
		int lenth=s.length(),u=0;
		T.build(1,1,lenth);
		for(int i=1;i<=lenth;i++){
			int k=s[i-1]-'a';
			u=ch[u][k];dp[i]=inf;
			if(biao[u]){
				if(i-biao[u]==0)dp[i]=1;
				else dp[i]=T.get_minn(1,1,lenth,i-biao[u],i-1)+1;
			}
			T.modify(1,1,lenth,i,dp[i]);
		}
		return dp[lenth]>=inf?-1:dp[lenth];
	}
}A;
int n;
string t; 
int main(){
	n=read();cin>>t;
	for(int i=1;i<=n;i++){
		string s;cin>>s;
		A.insert(s);
	} 
	A.getfail();
	cout<<A.find(t)<<endl;
    return 0;

}

4.[JSOI2007]文本生成器
题解:

时间复杂度为\(O(状态数*转移)\)

加强版为\(m\leq 10^4\)
考虑优化
我们可以将转移优化下,由\(O(100*26)->O(26)\)
我们可以预处理当前点转移到哪

注意某个节点是否可行,得看它的所有fail树上都是否可行

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+11;
const int MOD=10007;
const int inf=2147483647-2;
const double pi=acos(-1);
const double eps=1e-8;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int power(int x,int y){
	int ans=1;
	while(y){
		if(y&1)ans=ans*x%MOD;
		y>>=1;x=x*x%MOD;
	}
	return ans;
}
struct ACAM{
	int tot,ch[maxn][27],fail[maxn],biao[maxn];
	
	void insert(string s){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'A';
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		biao[u]=1;
		return ;
	} 
	
	void getfail(){
		queue<int>q;
		for(int i=0;i<26;i++)if(ch[0][i])q.push(ch[0][i]);
		while(!q.empty()){
			int u=q.front();q.pop();
			for(int i=0;i<26;i++){
				if(ch[u][i]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
					biao[ch[u][i]]|=biao[fail[ch[u][i]]]; //fail树上有结尾标记,当前点也是不可行的 
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	int dp[2][maxn];
	int get_dp(int lenth){
		dp[0][0]=1;
		for(int i=1;i<=lenth;i++){
			for(int u=0;u<=tot;u++)dp[i&1][u]=0;
			for(int u=0;u<=tot;u++)for(int k=0;k<26;k++){
				if(biao[ch[u][k]])continue;
				(dp[i&1][ch[u][k]]+=dp[(i-1)&1][u])%=MOD;
			}
		}	
		int ans=0;
		for(int i=0;i<=tot;i++)(ans+=dp[lenth&1][i])%=MOD;
		return ans;
	}
}A;

int main(){
	int n=read(),m=read();	
	for(int i=1;i<=n;i++){
		string s;cin>>s;
		A.insert(s);
	}
	A.getfail();
	printf("%lld\n",((power(26,m)-A.get_dp(m))%MOD+MOD)%MOD);
    return 0;
}

5.DNA Sequence
题意:

题解:


这道题跟上一道题有点类似,这道题给的串很短,要找的串很长,所以可以用矩阵优化

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<string>
#include<cmath>
#include<queue>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+11;
const int MOD=100000;
const int inf=2147483647-2;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int len;
struct Matrix{
    ll a[103][103];			//矩阵大小记得修改!!! 
    void init(){
        for(int i=0;i<len;i++){
            for(int j=0;j<len;j++)a[i][j]=(i==j);
        }
    }
    void zero(){memset(a,0,sizeof(a));}
    Matrix operator*(Matrix m2){
        Matrix m;m.zero();
        for(int i=0;i<len;i++){
            for(int j=0;j<len;j++){
                for(int k=0;k<len;k++){
                    (m.a[i][j]+=a[i][k]*m2.a[k][j]%MOD)%=MOD;
                }
            }
        }
        return m;
    }
    Matrix operator^(ll y){
        Matrix sum,x;sum.init();
        memcpy(x.a,a,sizeof(a));
        while(y){
            if(y&1)sum=sum*x;
            y>>=1;x=x*x;
        }
        return sum;
    }
};

struct ACAM{
	int tot,ch[maxn][4],fail[maxn],biao[maxn];
	void init(){
		for(int i=0;i<=tot+5;i++){
			for(int j=0;j<4;j++)ch[i][j]=0;
			fail[i]=0;biao[i]=0;
		}
		tot=0;
		return ;
	}
	int idx(char ch){
		switch(ch){
			case 'A':return 0;
			case 'C':return 1;
			case 'T':return 2;
			case 'G':return 3;
		}
		return 0;
	}
	void insert(string s){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=idx(s[i]);
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		biao[u]=1;
		return ;
	} 
	
	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]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
					biao[ch[u][i]]|=biao[fail[ch[u][i]]];
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	
	int solve(int lenth){
		Matrix t;len=0;
		vector<int>f(tot+1);
		for(int u=0;u<=tot;u++){
			if(biao[u])continue;
			f[u]=len++;			 
		}
		for(int u=0;u<=tot;u++){
			if(biao[u])continue;
			for(int i=0;i<4;i++){
				if(biao[ch[u][i]])continue;
				t.a[f[u]][f[ch[u][i]]]++;
			}
		}
		t=t^lenth;
		int ans=0;
		for(int i=0;i<len;i++)(ans+=t.a[0][i])%=MOD;
		return (ans%MOD+MOD)%MOD;
	}
}A;

int main(){
	int m,n;
	while(~scanf("%d%d",&m,&n)){
		for(int i=1;i<=m;i++){
			string s;cin>>s;
			A.insert(s);
		}
		A.getfail();
		cout<<A.solve(n)<<endl;
		A.init();
	}
    return 0;
}

如果要求长度小于等于n的不包含给定串的方案数,该如何求?



6.病毒
是否存在一个无限长的不包含给定串的字符串
等价于在AC自动机上走,不走给定字符串结尾点,是否能走出一个环
直接dfs

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+11;
const int MOD=10007;
const int inf=2147483647-2;
const double pi=acos(-1);
const double eps=1e-8;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

struct ACAM{
	int tot,ch[maxn][27],fail[maxn],biao[maxn];
	
	void insert(string s){
		int lenth=s.length(),u=0;
		for(int i=0;i<lenth;i++){
			int k=s[i]-'0';
			if(!ch[u][k])ch[u][k]=++tot;
			u=ch[u][k];
		}
		biao[u]=1;
		return ;
	} 
	
	void getfail(){
		queue<int>q;
		for(int i=0;i<26;i++)if(ch[0][i])q.push(ch[0][i]);
		while(!q.empty()){
			int u=q.front();q.pop();
			for(int i=0;i<2;i++){
				if(ch[u][i]){
					q.push(ch[u][i]);
					fail[ch[u][i]]=ch[fail[u]][i];
					biao[ch[u][i]]|=biao[fail[ch[u][i]]];
				}
				else ch[u][i]=ch[fail[u]][i];
			}
		}
		return ;
	} 
	
	int vis[maxn];
	int dfs(int x){
		if(vis[x]==1)return 1;
		if(vis[x]==-1)return 0;
		vis[x]=1;int now=0;
		for(int i=0;i<2;i++){
			if(biao[ch[x][i]])continue;
			now|=dfs(ch[x][i]);
			if(now)return 1;
		}
		vis[x]=-1;
		return 0;
	}
}A;

int main(){
	int n=read();
	for(int i=1;i<=n;i++){
		string s;cin>>s;
		A.insert(s);
	}
	A.getfail();
	if(A.dfs(0))puts("TAK");
	else puts("NIE");
    return 0;
}
posted @ 2022-10-07 14:03  I_N_V  阅读(59)  评论(0编辑  收藏  举报