突然发现了一篇很棒的讲解ac自动机的文章https://www.cnblogs.com/nullzx/p/7499397.html,补上!~

终于get了一个新算法(。ì _ í。) 

自己写了一遍之后结果结果是错的,debug半天才发现有个句子放错位置了,还是不够熟悉吧555

必备算法:KMP(失配时从哪里开始匹配才是最优的)+trie(字典树建树)

ac自动机的图如下:

 具体过程我就不是很想讲了,可以看b站的视频,我觉得讲的蛮明白的:传送门

几个关键点:

  • 当前节点的fail指向的是父节点fail下面和当前节点字母相同的节点:比如长字符串是shea,当和she匹配时,因为e后面没有a,所以e这个位置匹配失败,e(最左侧的e)的fail指向的是它的父节点h(最左侧的h)的fail:h(中间的h)下面的e(中间的e),然后在这个新的e(中间的e)的基础上向下匹配,可以匹配到a
  • 节点的fail指向位置的字母和节点对应的字母相同
  • 节点e的fail指向的位置(记为e'),e'的前缀是e前面字符串的后缀,当然也可以是空
  • 建立fail时要先从父节点的fail往下找,找不到再找父节点fail的fail
  • 在计数时每个节点都要算一下它(包括它的fail,fail的fail)的前缀是否是题里给出的单词,是的话将其标志置为-1,避免重复访问。

hdu2222ac代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<string>
#include<cmath>
#include<cstring>
const int maxn=1e6+6;
const int maxt=5e5+5;
using namespace std;
struct node{
    int next[30];
    int fail,cnt;//cnt记录出现当前节点对应的单词的次数
}state[maxt];

queue<int> q;//存字母的编号
int size;
char s[maxn];

void init()
{
    while(!q.empty()) q.pop();
    for(int i=0;i<maxt;i++)
    {
        memset(state[i].next,0,sizeof(state[i].next));
        state[i].cnt=state[i].fail=0;
    }
    size=1;//只有根节点
}

void insert(char *s)
{
    int len=strlen(s),now=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(!state[now].next[id]) state[now].next[id]=size++;
        now=state[now].next[id];
    }
    state[now].cnt++;//单词节点
}

void build()
{
    state[0].fail=-1;//根节点
    q.push(0);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=0;i<26;i++)
        {
            int now=state[u].next[i];//当前节点的编号
            if(now)//有这个字母节点
            {
                if(u==0) state[now].fail=0;//第一层都指向根节点
                else
                {
                    int v=state[u].fail;//父节点的fail
                    while(v!=-1)
                    {
                        if(state[v].next[i])
                        {
                            state[now].fail=state[v].next[i];//指向父节点下面和当前节点字母相同的节点
                            break;
                        }
                        v=state[v].fail;//一直找,父节点fail位置的字母和父节点的字母相同
                    }
                    if(v==-1) state[now].fail=0;//没找到指向根
                }
                q.push(now);
            }
        }
    }
}

int getnum(int u)
{
    int ans=0;
    while(u)
    {
        if(state[u].cnt!=-1) ans+=state[u].cnt;
        state[u].cnt=-1;//标记,避免重复访问
        u=state[u].fail;//顺着fail那条线一直找下去,因为可能第一次fail对应的字符串(这个字符串可以不是给出的单词)的后缀可能是题里给出的单词
    }
    return ans;
}

int match(char *s)
{
    int len=strlen(s),now=0,ans=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(state[now].next[id]) now=state[now].next[id];//有当前节点继续往下找
        else//失配
        {
            int p=state[now].fail;
            while(p!=-1 && state[p].next[id]==0) p=state[p].fail;
            if(p==-1) now=0;//没找到,回到根,从根开始找s中的下一个字母
            else now=state[p].next[id];
        }
        if(state[now].cnt!=-1)//每次找到一个字母节点都要计算,这个节点可以不是单词节点
            ans+=getnum(now);
    }
    return ans;
}
//输出字典树上字母的编号和fail,检查build是否正确
/*void out()
{
    cout<<"编号:"<<"fail:"<<endl;
    queue<int> qq;
    qq.push(0);
    while(!qq.empty())
    {
        int x=qq.front();qq.pop();
        for(int i=0;i<26;i++)
        {
            if(state[x].next[i])
            {
                cout<<state[x].next[i]<<"    "<<state[state[x].next[i]].fail<<endl;
                qq.push(state[x].next[i]);
            }

        }
    }
}*/
int main()
{
    //freopen("/Users/zhangkanqi/Desktop/11.txt","r",stdin);
    int t;
    scanf("%d",&t);
    while(t--)
    {
        init();
        int n;
        scanf("%d",&n);
        for(int i=0;i<n;i++)
        {
            scanf("%s",s);
            insert(s);
        }
        build();//建fail
       // out();
        scanf("%s",s);
        printf("%d\n",match(s));
    }
    return 0;
}