AC自动机

考虑基础的找寻两个串之间的最长匹配(其一之前缀与另一个串子串匹配)怎么办,这个可以用KMP去做。
但是如果我们要找一个串对于多个串的最长匹配,就不能用KMP来做了。
这时就要AC自动机来解决。
先说AC自动机的基本思想,首先我们根据所有待匹配的串建立一个trie树。
然后在trie树上建立fail指针,即为在该状态下,下一个字符不匹配会跳转到哪个状态。
因为我们要找的是最长匹配,所以说fail指针类似于next指针,它会跳到舍弃部分前缀之后的最长状态。
这就可以类比为一个树上KMP的过程。
这时将一个串放入自动机内跑之后,得到的就是以每一个字符作为末尾,保留向前的若干字符后对应在trie树上能够到达的最优状态。
那么现在有两个问题,怎么建出fail指针,怎么匹配。
我们现在假设一个点的fail指针已经确定,那么它再向后拼上一个字符时有两种情况。
第一种,向后拼上一个字符后,这个串存在,即该状态存在。
这时我们将下一个点的fail指针指向这个节点的fail指针指向的点再拼接一个相同字符的状态。
但是就有问题了,如果fail指针所指的点向下没有拼上该字符对应的状态时,我们还要再跳fail指针,这就会导致时间复杂度不对。
这时我们就要考虑第二种情况,该状态不存在。
其实可以发现,因为我们的下个状态用指针来存储,那么我们大可以让该状态指向之前的一个实际状态。
如果该状态不存在,那么它可以直接指向它为拼上该字符的状态的fail指针所指向的点再拼上一个字符对应的状态。
这时就会发现,不论指向的那个点是否真正存在,那个点对应指向的指针,一定是一个实际存在的状态。
那么时间复杂度就可以保证了,因为建立fail的跳转有且只有一次。
最高的时间复杂度是\(O(n×k)\)(n是所有串字符数之和,k是字符种类数)。
再来考虑匹配的过程。
现在将trie树已经建好,将一个串直接放在里面跑匹配。
每个字符处能够对应到一个状态,假设从一个点前所有的字符均匹配到了一个状态,而后一个字符失配。
这时发现因为我们建立状态时,直接指向了失去部分前缀的最优状态,我们就直接跳至该状态指向的状态即可。
匹配时更不用说,直接跳转即可,最后每个点到达的最长状态均可标记上。
这时根据每个点的fail指针对应的fail树跑一个拓扑排序即可统计出每个串的最长匹配。
这个的模板题是玄武密码
这里因为单个串的长度较小,所以说trie树深度小,这样直接在匹配至某个状态时暴力跳fail进行统计即可。
如果没有该优秀的深度限制,就老老实实用拓扑排序统计就好了。

#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
#define qr qr()
#define ps push_back
#define pa pair<int,int>
#define ve vector
#define fi first
#define se second
using namespace std;
const int N=1e7+200,M=2e5+200;
int n,m,id[M],cnt;
inline ll qr{
	ll x=0;char ch=getchar();bool f=0;
	while(ch>57||ch<48)f=(ch=='-')?1:0,ch=getchar();
	while(ch>=48&&ch<=57)x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}
char s[N],ss[105];
struct node{
	int son[4],id,fa,f,ans,dp;
}t[N];
int get(char a){
	return (a=='E')?0:((a=='S')?1:((a=='W')?2:3));
}
void insert(int x){
	int u=1,len=strlen(ss);
	for(int i=0;i<len;++i){
		int k=get(ss[i]);
		if(!t[u].son[k])t[u].son[k]=++cnt;
		u=t[u].son[k];t[u].dp=i+1;
	}id[x]=u;
}
void build(){
	queue<int>q;
	for(int i=0;i<4;++i)t[0].son[i]=1;
	q.push(1);t[1].fa=0;
	while(q.size()){
		int u=q.front(),fa=t[u].fa;
		// cout<<u<<endl;
		q.pop();
		for(int i=0;i<4;++i){
			if(!t[u].son[i]){
				t[u].son[i]=t[fa].son[i];
				continue;
			}int v=t[u].son[i];
			t[v].fa=t[fa].son[i];
			q.push(v);
		}
	}
}
void ask(){
	int len=strlen(s),u=1;
	for(int i=0;i<len;++i){
		int k=get(s[i]),tmp;
		tmp=u=t[u].son[k];
		while(tmp)t[tmp].f=1,tmp=t[tmp].fa;
	}
}
void dfs(int u){
	// cout<<u<<endl;
	for(int i=0;i<4;++i){
		int v=t[u].son[i];
		if(t[v].dp<=t[u].dp)continue;
		dfs(v);t[u].f=t[v].f||t[u].f;
	}
}
void dfs2(int u,int dp){
	t[u].ans=dp;
	for(int i=0;i<4;++i){
		int v=t[u].son[i];
		if(t[v].dp<=t[u].dp)continue;
		dfs2(v,dp+t[v].f);
	}
}
void init(){
	cin>>n>>m;
	cin>>s;cnt=1;
	for(int i=1;i<=m;++i)cin>>ss,insert(i);
	build();
	ask();
	dfs(1);//cout<<"DSNFG"<<endl;
	dfs2(1,0);
	for(int i=1;i<=m;++i)cout<<t[id[i]].ans<<'\n';
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);




	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);




	init();
	return 0;
}

很多时候,一些题会让我们在AC自动机上做dp或者统计,这个时候我们需要去注意几个性质。
第一是一个节点的fail指针指向的状态往往是可以被它本身所覆盖或统计的。
第二是在我们优化建边之后,所有的指针都是状态之间的转移。
最短母串这道题,就是在AC自动机上的状压dp。
我们将每个串是否被访问过的状态压缩,然后在AC自动机上去跑。
这里就需要我们把fail指针所指向的节点的状态直接给到该节点上。
这时候在跑dp时,有意义的就只有我们优化建边后的边(跳fail就完全没必要),直接用这些边去转移即可。

#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
#define qr qr()
#define ps push_back
#define pa pair<int,int>
#define ve vector
#define fi first
#define se second
using namespace std;
const int N=2e5+200;
int n,m,cnt,f[650][5000];
pa la[650][5000];
inline ll qr{
	ll x=0;char ch=getchar();bool f=0;
	while(ch>57||ch<48)f=(ch=='-')?1:0,ch=getchar();
	while(ch>=48&&ch<=57)x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}
char s[55];
struct node{
	int fa,son[26],f;char ch;
}t[N];
void insert(int x){
	int len=strlen(s),u=1;
	for(int i=0;i<len;++i){
		if(!t[u].son[s[i]-'A'])t[u].son[s[i]-'A']=++cnt;
		u=t[u].son[s[i]-'A'];t[u].ch=s[i];
	}t[u].f|=1<<x;
}
void build(){
	queue<int>q;
	for(int i=0;i<26;++i)t[0].son[i]=1;
	q.push(1);
	while(q.size()){
		int u=q.front(),fa=t[u].fa;
		q.pop();
		for(int i=0;i<26;++i){
			int v=t[u].son[i];
			if(!v){t[u].son[i]=t[fa].son[i];continue;}
			t[v].fa=t[fa].son[i];
			t[v].f|=t[t[v].fa].f;q.push(v);//fail指针直接指向的子状态包含的,它本身就包含。
		}
	}
}
bool check(int x){
	return x==(1<<n)-1;
}
void print(pa u){
	int now=u.fi;
	u=la[u.fi][u.se];
	if(u.fi!=1)print(u);
	cout<<t[now].ch;
}
void bfs(){
	memset(f,0,sizeof(f));
	queue<pa>q;
	q.push({1,0});f[1][0]=0;
	while(q.size()){
		pa u=q.front();q.pop();
		// cout<<u.fi<<' '<<u.se;
		int tmp=u.fi,zt=u.se,dis=f[tmp][zt];
		if(check(u.se)){print(u);break;}
		for(int i=0;i<26;++i){
			int v=t[u.fi].son[i],now=zt|t[v].f;
			if(!f[v][now])q.push({v,now}),f[v][now]=dis+1,la[v][now]=u;
		}
	}
}
void init(){
	cin>>n;cnt=1;
	for(int i=1;i<=n;++i)cin>>s,insert(i-1);
	build();bfs();
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);




	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);




	init();
	return 0;
}
posted @ 2024-09-03 11:26  SLS-wwppcc  阅读(17)  评论(0)    收藏  举报