AC自动机相关专题

本文详细介绍了AC自动机的原理及其在字符串匹配问题中的应用,包括基础版、加强版和二次加强版。AC自动机是一种高效处理多个字符串在一个字符串上匹配的算法,通过构建字典树和fail指针实现快速查找。文章逐步解析了AC自动机的构造过程,优化方法以及在大规模数据下的处理策略,并提供了相关代码示例。通过对fail指针的优化,降低了匹配过程中的复杂度,提高了效率。
摘要由CSDN通过智能技术生成

时隔一年,重新开始回归博客。最近被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;

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值