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;
}