时隔一年,重新开始回归博客。最近被ac自动机折磨的死去活来,直到昨天做完了洛谷的二次加强版之后有所感悟,突然想写篇博客整理一下,本篇的题目来源于洛谷的ac自动机和其加强版以及二次加强版。
首先介绍一下ac自动机是用来干什么的,它是关于求解一种关于多个字符串在一个字符串上匹配问题。它需要的基础是字典树和kmp算法。
回忆一下kmp算法吧。俩个字符串相匹配,开始一个字母一个字母的匹配,如果可以匹配,则继续向后匹配,如若匹配不到,则模式串当前指针指向next[当前指针],这个next数组的作用就是加快匹配速度。而ac自动机中也有这样一个标志,我们称它为fail指针吧,它存在于字典树上的每一个节点。这个fail指针指向的模式串的一部分前缀与当前模式串的一部分后缀相同,且在在满足这个条件下,这个后缀最长(与kmp中next数组的含义类似)。我们在字典树上对主串进行匹配。如果当前节点有这个字母节点(字典树的含义),则这个节点跳到字母节点,主串匹配位置后移继续匹配。如果没有的话,跳到当前节点的fail值上,再查看是否有,若无,则一直跳,直到跳到0节点(根节点,不代表字母),如果还是没有,则主串后移继续匹配。通过这种方式,我们就可以找出所有在文本串中出现的模式串。
关于如何去建这个fail边,一般使用bfs,层层解决。具体我们看代码。
题目链接(ac自动机)
代码如下:
#include<cmath>
#include <iostream>
#include<stdio.h>
#include<string>
#include<string.h>
#include<vector>
#include<set>
#include<map>
#include<cstring>
#include<math.h>
#include<stack>
#include<algorithm>
#include<queue>
#include<bitset>
#include<sstream>
#include<iomanip>
#include<time.h>
#include<stdlib.h>
#include<unordered_map>
#define ll long long int
const ll mod=1e9+7;
const ll modd=233317;
const ll base=131;
const double pi=acos(-1);
const int N=2e5+10;
const int M=1e9;
const double esp=1e-6;
using namespace std;
ll k,tot=0,too=0,n,m;
string s;
struct node
{
ll pz[26],fail,id;
}t[2000100];
deque<ll> p;
void jianshu(string sx)
{
ll ap=0,i;
for(i=0;i<sx.length();i++)
{
if(!t[ap].pz[sx[i]-'a'])
{
tot++;
t[ap].pz[sx[i]-'a']=tot;
}
ap=t[ap].pz[sx[i]-'a'];
}
t[ap].id++;
}
void getfail()
{
ll ap=0,i;
for(i=0;i<26;i++)
{
if(t[ap].pz[i])
{
p.push_back(t[ap].pz[i]);
}
}
while(!p.empty())
{
ap=p.front();
p.pop_front();
ll fx=t[ap].fail;
for(i=0;i<26;i++)
{
if(t[ap].pz[i])
{
ll xp=fx;
while(xp&&!t[xp].pz[i])
xp=t[xp].fail;
t[t[ap].pz[i]].fail=t[xp].pz[i];
p.push_back(t[ap].pz[i]);
}
}
}
}
ll getans(string s)
{
ll ap=0,i,ans=0;
for(i=0;i<s.length();i++)
{
while(ap&&!t[ap].pz[s[i]-'a'])
ap=t[ap].fail;
ap=t[ap].pz[s[i]-'a'];
ll fx=ap;
while(fx&&t[fx].id!=-1)
{
ans+=t[fx].id;
t[fx].id=-1;//避免重复更新。
fx=t[fx].fail;
}
}
return ans;
}
int main()
{
ll zt,i,j,flat=0,iz=0,l,r,id,ma,ip,x,y;
//freopen("C:\\Users\\86189\\Desktop\\shujux.txt","r",stdin);
//freopen("C:\\Users\\86189\\Desktop\\shuju2.txt","w",stdout);
ios::sync_with_stdio(false);
cin>>n;
for(i=0;i<n;i++)
{
cin>>s;
jianshu(s);
}
getfail();
cin>>s;
ma=getans(s);
cout<<ma<<endl;
return 0;
}
我们来看这个getfail()函数,队列中的节点的fail值是已经求出的,而我们要做的是拿他来更新它的子节点。这个fx是当前节点的fail边连的节点,如果它也有i这个儿子,则当前节点的儿子节点的fail连向的就是fx的i的儿子。如果没有,我们应该从fx的fail连向的节点继续找是否有i这个儿子。重复这个过程,直到找到或者到根节点还没有找到。
至于匹配函数getans(),前面已经讲了如何匹配,这个题的话在这种写法下,每找到一个节点,我们都要循环的去找它fail连的点,因为如果当前字符串匹配到了,那么fail所连的点所在的字符串一定可以匹配到,且是当前匹配到的字符串的最长后缀。
有没有感觉其实复杂度挺高的,而且容易被卡掉!
在这里我们可以优化,我们发现在getfail()这个函数中,循环往复的去找到底哪个节点与当前节点一样有i儿子非常的多余。我们可不可以让这个过程一步到位。那么如果当前节点有i儿子的话,这个儿子点的fail直接连向父节点的fail点的i儿子。如果没有的话,这个儿子节点直接指向fail的i儿子。这样我们在getans()这个函数中,不需要一直跳去寻找匹配的点了,只需要跳一步就能找到匹配的点,如果不匹配,则直接找到根节点。下面放一个ac自动机的加强版的代码(优化过的)。
题目链接(ac自动机加强版)
代码如下
#include<cmath>
#include <iostream>
#include<stdio.h>
#include<string>
#include<string.h>
#include<vector>
#include<set>
#include<map>
#include<cstring>
#include<math.h>
#include<stack>
#include<algorithm>
#include<queue>
#include<bitset>
#include<sstream>
#include<iomanip>
#include<time.h>
#include<stdlib.h>
#include<unordered_map>
#define ll int
const ll mod=1e9+7;
const ll modd=233317;
const ll base=131;
const double pi=acos(-1);
const int N=2e5+10;
const int M=1e9;
const double esp=1e-6;
using namespace std;
ll k,tot=0,too=0,n,m,ans[2000];
struct node
{
ll c[26],id,fail;
} t[1000100];
string s[200];
deque<ll> p;
void innt()
{
memset(t,0,sizeof(t));
memset(ans,0,sizeof(ans));
}
void jianshu(string sx,ll id)
{
ll ap=0,i;
for(i=0;i<sx.length();i++)
{
if(!t[ap].c[sx[i]-'a'])
{
tot++;
t[ap].c[sx[i]-'a']=tot;
}
ap=t[ap].c[sx[i]-'a'];
}
t[ap].id=id;
}
void chuli()
{
ll ap=0,i,j;
for(i=0;i<26;i++)
{
if(t[ap].c[i])
{
p.push_back(t[ap].c[i]);
}
}
while(!p.empty())
{
ap=p.front();
p.pop_front();
ll fx=t[ap].fail;
for(i=0;i<26;i++)
{
ll y=t[ap].c[i];
if(y)
{
t[y].fail=t[fx].c[i];
p.push_back(y);
}
else
{
t[ap].c[i]=t[fx].c[i];
}
}
}
}
void pipei(string sx)
{
ll ap=0,i;
for(i=0;i<sx.length();i++)
{
ap=t[ap].c[sx[i]-'a'];
ll fa=ap;
while(fa)
{
if(t[fa].id)
{
ans[t[fa].id]++;
}
fa=t[fa].fail;
}
}
}
int main()
{
ll zt,i,j,flat=0,iz=0,l,r,id,ma,ip,x,y;
//freopen("C:\\Users\\86189\\Desktop\\shujux.txt","r",stdin);
//freopen("C:\\Users\\86189\\Desktop\\shuju2.txt","w",stdout);
ios::sync_with_stdio(false);
while(cin>>n)
{
if(n==0)
break;
innt();
tot=0;
for(i=1;i<=n;i++)
{
cin>>s[i];
jianshu(s[i],i);
}
chuli();
ma=0;
cin>>s[0];
pipei(s[0]);
for(i=1;i<=n;i++)
{
ma=max(ma,ans[i]);
}
cout<<ma<<endl;
for(i=1;i<=n;i++)
{
if(ma==ans[i])
cout<<s[i]<<endl;
}
}
return 0;
}
一篇草稿保持了半个月,今天在动车上更完。
在彻底学懂了这个算法以后,我们来看二次加强版。
题目链接(ac自动机二次加强版)
这个题和上一个几乎一样,但是数据量非常的大。我们是不是可以再考虑如何优化计算。我们可以发现,每次找到一个匹配的字符串的话,我们会将它的所有的后缀子串都更新计算一遍,这个过程非常的冗余,我们是不是可以找到一个字符串以后把这个节点标记下来,到最后进行统一更新呢?答案是显然可以的,但是我们需要从叶子节点开始往上更新,因为要拿尽可能长的串往短的串上更新(若长串被匹配过,则它的短后缀一定可以被匹配)。所以我们将这个点与这个点的fail点连一条单向边,从当前节点到fail节点,然后根据他们的拓扑关系更新即可(拓扑序大家应该会吧)。注意这个题不保证每个字符串各不相同!!!
代码如下:
#include<cmath>
#include <iostream>
#include<stdio.h>
#include<string>
#include<string.h>
#include<vector>
#include<set>
#include<map>
#include<cstring>
#include<math.h>
#include<stack>
#include<algorithm>
#include<queue>
#include<bitset>
#include<sstream>
#include<iomanip>
#include<time.h>
#include<stdlib.h>
#include<unordered_map>
#define ll int
const ll mod=1e9+7;
const ll modd=233317;
const ll base=131;
const double pi=acos(-1);
const int N=2e5+10;
const int M=1e9;
const double esp=1e-6;
using namespace std;
ll k,tot=0,too=0,n,m,ans[200100],fa[200100],a[2000010],dfn[2000010];
struct node
{
ll c[26],id,next,biao;//next代表博文中的fail
} t[2000100];
struct nodd
{
ll y,next;
}pz[2000020];
void add(ll x,ll y)
{
too++;
pz[too].next=a[x];
pz[too].y=y;
a[x]=too;
}
string s[200010];
deque<ll> p;
void innt()
{
memset(t,0,sizeof(t));
memset(ans,0,sizeof(ans));
memset(fa,0,sizeof(fa));
memset(dfn,0,sizeof(dfn));
for(ll i=0;i<2000001;i++)
{
a[i]=-1;
}
}
void jianshu(string sx,ll id)
{
ll ap=0,i;
for(i=0;i<sx.length();i++)
{
if(!t[ap].c[sx[i]-'a'])
{
tot++;
t[ap].c[sx[i]-'a']=tot;
}
ap=t[ap].c[sx[i]-'a'];
}
fa[id]=ap;
}
void chuli()
{
ll ap=0,i,j;
for(i=0;i<26;i++)
{
if(t[ap].c[i])
{
p.push_back(t[ap].c[i]);
}
}
while(!p.empty())
{
ap=p.front();
p.pop_front();
ll fx=t[ap].next;
for(i=0;i<26;i++)
{
ll y=t[ap].c[i];
if(y)
{
t[y].next=t[fx].c[i];
p.push_back(y);
}
else
{
t[ap].c[i]=t[fx].c[i];
}
}
}
}
void pipei(string sx)
{
ll ap=0,i;
for(i=0;i<sx.length();i++)
{
ap=t[ap].c[sx[i]-'a'];
ll fa=ap;
t[ap].biao++;
}
for(i=1;i<=tot;i++)
{
if(t[i].next)
{
add(i,t[i].next);
dfn[t[i].next]++;
}
}
for(i=1;i<=tot;i++)
{
if(dfn[i]==0)
{
p.push_back(i);
}
}
while(!p.empty())
{
ll x=p.front();
p.pop_front();
for(i=a[x];i!=-1;i=pz[i].next)
{
ll y=pz[i].y;
t[y].biao+=t[x].biao;
dfn[y]--;
if(dfn[y]==0)
p.push_back(y);
}
}
}
int main()
{
ll zt,i,j,flat=0,iz=0,l,r,id,ma,ip,x,y;
//freopen("C:\\Users\\86189\\Desktop\\shujux.txt","r",stdin);
//freopen("C:\\Users\\86189\\Desktop\\shuju2.txt","w",stdout);
ios::sync_with_stdio(false);
cin>>n;
innt();
tot=0;
for(i=1;i<=n;i++)
{
cin>>s[i];
jianshu(s[i],i);
}
chuli();
ma=0;
cin>>s[0];
pipei(s[0]);
for(i=1;i<=n;i++)
{
cout<<t[fa[i]].biao<<endl;
}
return 0;
}