匹配多个字符串——AC自动机

真·AC自动机:zyy
AC自动机这个东西。。名字真的是没得吐槽=_=
它运用了kmp算法的next思想(体现于后文的fail指针)以及trie的数据结构(字母树)
kmp如果不会的话还可以,trie不会的话。。建议还是先去学trie吧
我看了好久。。才看懂这个蜜汁自动机


先看一下模版题来了解AC自动机的用处:
Hdu2222
题意:有n段小字符串,求其中有多少段出现在了一个大字符串中
题目应该不难理解
那么怎么做?
朴素想法:每一个字符串去大字符串中kmp一遍之间效率O(n*len)
。。妥妥tle

对于这种问题,我们就需要用到AC自动机
AC自动机=trie+fail
所谓fail指针,就是一个类似于kmp中的next,用于匹配失败时快速跳转。
虽然不知道为什么别的为什么每个例子都一样。。那我也一样好了
大概是什么 she he shr say her..??
然后大字符串: yasheshr?
差不多就这样…
我们先把所有小字符串放到trie中,这一步不详细解释了

inline void insert(string ts)
{
    int len=ts.length();
    int p=0;
    For(i,0,len-1)
    {
        int c=ts[i]-'a';
        if(!nxt[p][c])  nxt[p][c]=++tot;
        p=nxt[p][c];
    }
    cnt[p]++;
}

接下来我们考虑一下大字符串在trie上的询问
一条一条走下去。。走不了了就回溯?
累加各个结束标记?
时间效率。。O(结点数)
好像和直接kmp没什么太大局别- -…
但是其实有很多回溯都是没有用的
很多很多!!
不只是这样
我们根据刚才的输入构建出来的trie应该是这样的:
这里写图片描述
我们搜完she后,如果是用回溯,就会找不到r在哪=_=
所以不只是时间上,连思想上都退化成了枚举。。
那么要怎么办?
我们可不可以在搜完she后直接跳到he后面那个r呢
。。。这个想法似曾相识对吧= =
没错它就是kmp
fail指针就是这个用处,直接指向he后面那个r
这个其实。。说实话,构造过程只可意会不可言传
好吧我还是言传一下 不然要被打
一开始,所有节点的fail指针指向root(根节点)
我们如果要更新节点t的fail,那么我们就沿着t的父亲的fail指针一直走啊走,走到一个节点有t这个字母的儿子
比如说我们要找上图h的fail
这里写图片描述
我们通过S的fail询问到根节点,发现其有一个是叫做 H的儿子
那么我们就把第三层的h的fail指向第二次的那个h
这里写图片描述
同样,我们要找E的fail
这里写图片描述
通过E的父节点H的fail,找到第二层的H,发现其有一个儿子为E,就可以更新fail指针了
这里写图片描述
啊。。吐血
还是看代码理解一下吧,这个真的不知道应该怎么说
补:lc233大佬说,为什么不能用dfs求,那么下面我放两张图大家理解一下。。
这里写图片描述
然后我们要求最深的那个E的fail
此时更新了第一条链的fail
这里写图片描述
这就是为什么dfs不可行。。。

inline void build()
{
    int t,l=1,r=1;
    fail[root]=0;
    q[1]=root;
    while(l<=r)
    {
        t=q[l];
        int p=0;
        For(i,0,25)
        {
            if(nxt[t][i])
            {
                if(t==root){fail[nxt[t][i]]=root;}
                else
                {
                    p=fail[t];
                    while(p)
                    {
                        if(nxt[p][i])
                        {
                            fail[nxt[t][i]]=nxt[p][i];
                            break;
                        }
                        p=fail[p];
                    }

                    if(!p)  
                    {
                        if(nxt[p][i]) fail[nxt[t][i]]=nxt[p][i];
                        else fail[nxt[t][i]]=root;
                    }
                }
                q[++r]=nxt[t][i];
            }
        }
        l++;
    }
    For(i,1,r)  q[i]=0;
}

然后询问的时候,我们只需要一路搜下去,如果匹配失败就往fail那边跑,一路把结束标记全部加起来。。就是答案了
要注意!重复的千万不能加!
当我们加到一个点时,可以用一个while循环将他所有fail指针上的end指针加上去

inline void query(string s)
{
    int len=s.length(),sum=0,p=0;
    For(i,0,len-1)
    {
        int c=s[i]-'a';
        while(nxt[p][c]==0&&p)  p=fail[p];
        p=nxt[p][c];
        int t=p;
        while(t&&cnt[t]!=-1)
        {
            sum+=cnt[t];
            cnt[t]=-1;
            t=fail[t];
        }
    }
    printf("%d\n",sum);
}

好了那么大致思路就是这样了
附HDU2222 AC代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<string>
#include<cstring>
#define inf 1e9
#define ll long long
#define For(i,j,k) for(int i=j;i<=k;i++)
#define Dow(i,j,k) for(int i=k;i>=j;i--)
using namespace std;
int n,nn,nxt[1000001][30],q[1000001],fail[1000001],cnt[1000001],tot,root=0;
inline void insert(string ts)
{
    int len=ts.length();
    int p=0;
    For(i,0,len-1)
    {
        int c=ts[i]-'a';
        if(!nxt[p][c])  nxt[p][c]=++tot;
        p=nxt[p][c];
    }
    cnt[p]++;
}
inline void build()
{
    int t,l=1,r=1;
    fail[root]=0;
    q[1]=root;
    while(l<=r)
    {
        t=q[l];
        int p=0;
        For(i,0,25)
        {
            if(nxt[t][i])
            {
                if(t==root){fail[nxt[t][i]]=root;}
                else
                {
                    p=fail[t];
                    while(p)
                    {
                        if(nxt[p][i])
                        {
                            fail[nxt[t][i]]=nxt[p][i];
                            break;
                        }
                        p=fail[p];
                    }

                    if(!p)  
                    {
                        if(nxt[p][i]) fail[nxt[t][i]]=nxt[p][i];
                        else fail[nxt[t][i]]=root;
                    }
                }
                q[++r]=nxt[t][i];
            }
        }
        l++;
    }
    For(i,1,r)  q[i]=0;
}
inline void query(string s)
{
    int len=s.length(),sum=0,p=0;
    For(i,0,len-1)
    {
        int c=s[i]-'a';
        while(nxt[p][c]==0&&p)  p=fail[p];
        p=nxt[p][c];
        int t=p;
        while(t&&cnt[t]!=-1)
        {
            sum+=cnt[t];
            cnt[t]=-1;
            t=fail[t];
        }
    }
    printf("%d\n",sum);
}
inline void doit()
{
    For(i,0,tot)
        For(j,0,25) nxt[i][j]=0;
    For(i,0,tot)    fail[i]=cnt[i]=0;
    tot=root=0;
    cin>>n;
    For(i,1,n)
    {
        string ts;
        cin>>ts;
        insert(ts);
    }
    build();
    string s;
    cin>>s;
    query(s);
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>nn;
    For(ii,1,nn)
    {
        doit();
    }
}
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值