AC自动机学习笔记

AC自动机经常有人说是在字典树上看毛片 KMP,那么,到底是什么呢?我们仔细来看看吧!!!假装有背景,有气势。。。

先庆祝一波,作为一本通的最后一个字符串里的最后一章节,当然,没有后缀数组QAQ,当然也暂时不打算去学。。。

AC机简介

AC自动机?能够自动AC题目的算法?那岂不无敌,干脆叫AK自动机算了。。。不不不。AC自动机是Aho-Corasick automation发现的,该算法在1975年产生于贝尔实验室,是著名的多模式匹配算法之一。

其实本质是在Trie上跑Kmp,然后加一些优化,使他跑得飞快。

算法原理

例题

先将所有的模式串插入Trie。
这是前提。

然后提一下,在这里的fail指针跟kmp数组的定义不太一样,他定义的是以当前节点为终点的后缀最大能跟那个模式串的前缀相等,同时记录这个节点。
在这里插入图片描述
黄色就为fail指针所指向的节点。

如何构造?

我们发现,每个节点指向的都是层数比自己小的节点,我们可以用BFS遍历,一步一步处理层数越来越小的。

  1. 将0号节点的儿子加入list,然后设他的fail指针为0。
  2. 开始BFS,取出队头,循环一遍儿子节点,设\(now=fail_{当前的节点},c=儿子节点的字符\),判断now是否有字符为c的儿子,没有,\(now=fail_{now}\),不断下去,知道\(now==0\)或者有这个儿子的时候,儿子节点的fail就等于\(son[now]_{c}\),同时将儿子节点加入队列。

然后,匹配母串的时候,我们只需要不断的跳fail也就可以实现。
现在不多说,看不懂也没关系作者语文不好没办法,后面的优化会让你更加清楚与明白。

优化与实现

算法原理大家基本上已经很清晰了QMQ,语文不好QAQ但是这样跑起来会慢,更何况AC机是要解决多模式匹配的,慢一点全局都会崩端!

尤其是慢慢跳KMP的那个方法,很容易被卡,回顾一下我们是如何匹配的?

在这里插入图片描述

我们发现当前的now没有b这个儿子。

于是我们继续跳:
在这里插入图片描述

我们发现有b这个儿子,并让队首的儿子的fail等于了now的b儿子。

真棒棒!

但是大佬不满足现状,做了个大胆的假设:
在这里插入图片描述

如果在这一步的时候就匹配好了,就等于一次就匹配完毕了!

于是,我们考虑在将now取出队列的时候,当我们发现now没有这个儿子的时候,就让now的这个儿子代表\(fail_{now}\)的儿子。

于是,我们得到了一个图(全部匹配完):
在这里插入图片描述

绿色的代表这个节点没有这个儿子而去指向别人的儿子。

我们发现有个a自己指向了自己?因为他的fail指针是根节点,根节点的a儿子就是他自己,会不会有错误?不会,同时,字典树变成了字典图!

因此,我们得到每个节点的儿子的定义,代表跳到当前节点的这个儿子,如果原本没有,则是代表在不断跳fail的过程中,第一个有这个儿子的节点的这个儿子的编号,至于为什么正确,很好想,就不浪费章节了作者是真的懒。。。

给出BFS过程:

int  list[N],head,tail;
void  bfs()
{
    head=1;tail=1;//初始化
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];//将0号节点的儿子塞入队列
    }
    while(head!=tail)
    {
        int  x=list[head++];//取队头
        for(int  i=0;i<=25;i++)
        {
            int  y=tr[x].a[i];//找儿子
            if(y)//有儿子,为他匹配fail指针
            {
                int  p=tr[tr[x].f].a[i];tr[y].f=p;//匹配fail指针
                // if(tr[p].v)tr[y].last=p;
                // else  tr[y].last=tr[p].last;
                list[tail++]=y;//加入队列
            }
            else  tr[x].a[i]=tr[tr[x].f].a[i];//没有儿子,指向fail指针所指向节点的儿子
        }
    }
}

匹配母串也差不多,不再赘述。

也许有人问了,中间注释掉的两行是什么?

接下来讲的优化:last优化。

在例题中,我们要统计出现的单词数量,但是我们在匹配由于是目前匹配的最长长度,但有可能有的串已经出现了,但不是最长长度。

如图:
在这里插入图片描述

我们匹配到now的位置,并且答案++,但是我们发现ab的串也匹配成功了!于是我们需要不断的从当前匹配节点跳fail来加上所有已经匹配到的串。

这样是可以AC简单版的。

但是:
加强版。。。

给出题人寄刀片吧。。。

我们为何要不断的跳fail指针呢?快呀!

我们可以用last来记录最长的与这个节点的后缀相等的字符串的最后一个节点,我们只需要跳last就可以保证不会跳多余的次数了。

//简单版代码
//双倍经验!(https://loj.ac/problem/10057)

#include<cstdio>
#include<cstring>
#define  N 1100000
using  namespace  std;
struct  trie
{
    int  a[26],v,f,last;//v是以他为结尾的有多少字符串,f是fail指针,last是个优化
}tr[N];int  trlen,n;
char  st[N];
void  add()//字典树添加
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'a';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v++;
}
int  list[N],head,tail;
void  bfs()
{
    head=1;tail=1;//初始化
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];//将0号节点的儿子塞入队列
    }
    while(head!=tail)
    {
        int  x=list[head++];//取队头
        for(int  i=0;i<=25;i++)
        {
            int  y=tr[x].a[i];//找儿子
            if(y)//有儿子,为他匹配fail指针
            {
                int  p=tr[tr[x].f].a[i];tr[y].f=p;//匹配fail指针
                if(tr[p].v)tr[y].last=p;
                else  tr[y].last=tr[p].last;//匹配last
                list[tail++]=y;//加入队列
            }
            else  tr[x].a[i]=tr[tr[x].f].a[i];//没有儿子,指向fail指针所指向节点的儿子
        }
    }
}
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add();
    }
    bfs();//AC机
    scanf("%s",st+1);
    int  root=0,ans=0,m=strlen(st+1);
    for(int  i=1;i<=m;i++)
    {
        int  k=st[i]-'a';
        root=tr[root].a[k];//看看目前匹配到的节点是哪个?
        int  up=root;//统计答案
        while(up)
        {
            if(tr[up].v==-1)break;//如果这个节点被走过,代表前面的都走过,就不走了
            ans+=tr[up].v;tr[up].v=-1;//标记与统计
            up=tr[up].last;//继续跳
        }
    }
    printf("%d\n",ans);//输出。
    return  0;
}
//加强版代码
//注意,没有重复字符串

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  trie
{
    int  a[26],v,last,f;
}tr[11000];int  trlen;
int  bk[210];
char  st[210][110];
char  stc[1100000];
int  n,m;
void  add(int  id)
{
    int  len=strlen(st[id]+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[id][i]-'a';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    if(!tr[root].v)tr[root].v=id;
}
int  list[11000],head,tail;
void  bfs()//AC自动机板子
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  y=tr[x].a[i];
            if(y)
            {
                int  p=tr[tr[x].f].a[i];tr[y].f=p;
                if(tr[p].v)tr[y].last=p;
                else  tr[y].last=tr[p].last;
                list[tail++]=y;
            }
            else  tr[x].a[i]=tr[tr[x].f].a[i];
        }
    }
}
inline  int  mymax(int  x,int  y){return  bk[x]>bk[y]?x:y;}//取匹配数最大字符串编号
int  main()
{
    while(1)
    {
        memset(bk,0,sizeof(bk));
        memset(tr,0,sizeof(tr));trlen=0;//初始化
        scanf("%d",&n);
        if(n==0)break;//退出
        for(int  i=1;i<=n;i++)
        {
            scanf("%s",st[i]+1);
            add(i);//添加
        }
        bfs();//AC机
        scanf("%s",stc+1);m=strlen(stc+1);
        int  root=0;
        for(int  i=1;i<=m;i++)
        {
            int  k=stc[i]-'a';root=tr[root].a[k];//匹配
            int  up=root;
            while(up)bk[tr[up].v]++,up=tr[up].last;//统计
        }
        int  maxid=1;
        for(int  i=2;i<=n;i++)maxid=mymax(maxid,i);//找最大
        printf("%d\n",bk[maxid]);
        for(int  i=1;i<=n;i++)
        {
            if(bk[i]==bk[maxid])printf("%s\n",st[i]+1);//输出字符串
        }
    }
    return  0;
}

例题

一本通例题一跟luogu简单版是双倍经验。。。

练习一

记录每个节点在字典树中的father,然后再记录每个字符串结束位置的编号以及长度,然后在匹配过程中标记匹配到的节点,然后最后找一下,就行了。

#include<cstdio>
#include<cstring>
#define  N  21000000
#define  M  110000
using  namespace  std;
struct  node
{
    int  fa,a[4],f;//四个儿子,f是fail指针,fa是father
    bool  bk;
}tr[N];int  trlen,gtk[M],glen[M];
char  stc[11000000],st[110];
int  n,m;
inline  int  calc(char  ch)//计算
{
    if(ch=='E')return  0;
    else  if(ch=='S')return  1;
    else  if(ch=='W')return  2;
    else  return  3;
}
void  add(int  id)//添加第id个字符串
{
    int  root=0;glen[id]=strlen(st+1);
    for(int  i=1;i<=glen[id];i++)
    {
        int  k=calc(st[i]);
        if(!tr[root].a[k])tr[root].a[k]=++trlen,tr[trlen].fa=root;
        root=tr[root].a[k];
    }
    gtk[id]=root;//记录结束节点
}
int  list[N],head,tail;
void  bfs()
{
    head=tail=1;
    for(int  i=0;i<=3;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=3;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)
            {
                tr[y].f=p;
                list[tail++]=y;
                //这道题不用last
            }
            else  y=p;
        }
    }
}
int  main()
{
    scanf("%d%d",&n,&m);
    scanf("%s",stc+1);
    for(int  i=1;i<=m;i++)
    {
        scanf("%s",st+1);
        add(i);
    }
    bfs();//AC机准备
    int  root=0;
    for(int  i=1;i<=n;i++)
    {
        root=tr[root].a[calc(stc[i])];//匹配
        int  up=root;
        while(up)
        {
            if(tr[up].bk)break;//优化
            tr[up].bk=true;up=tr[up].f;//标记路过节点
        }
    }
    for(int  i=1;i<=m;i++)
    {
        int  zuxian=gtk[i],cnt=0;
        while(zuxian)//寻找最后的被标记过的节点
        {
            if(tr[zuxian].bk)break;
            cnt++;zuxian=tr[zuxian].fa;
        }
        printf("%d\n",glen[i]-cnt);//输出
    }
    return  0;
}

练习二

这道题与kmp的一道题很像,开个栈,由于不存在一个字符串是另一个字符串的子串,所以last没用。

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  trie
{
    int  a[26],f,v;//f是fail,v记录字符串长度
}tr[110000];int  trlen;//字典树
char  st[110000],stc[110000];
void  add()
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'a';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v=len;//记录
}
int  list[110000],head,tail;
void  bfs()//AC机板子
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)
            {
                tr[y].f=p;
                list[tail++]=y;
                //不用last
            }
            else  y=p;
        }
    }
}
char  zhanss[110000];//栈
int  zhan[110000]/*记录栈中匹配到哪个节点*/,zhlen/*栈长度*/,m,n;
int  main()
{
    scanf("%s",stc+1);m=strlen(stc+1);
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add();
    }
    bfs();
    for(int  i=1;i<=m;i++)
    {
        int  root=zhan[zhlen];
        root=tr[root].a[stc[i]-'a'];//匹配
        zhan[++zhlen]=root;zhanss[zhlen]=stc[i];//入栈
        if(tr[root].v)zhlen-=tr[root].v;//出栈
    }
    zhanss[zhlen+1]='\0';
    printf("%s\n",zhanss+1);//输出
    return  0;
}

练习三

当时我竟然还打算重构树?其实不用。

记录每个节点被经过的次数,然后在AC机的过程中,每个节点要把自己的附加权值加到自己fail指针指向的节点,当然,要先从层数高的加到层数低的。
为什么正确?当一个节点能直接或间接指向这个字符串,那么就代表答案++。

#include<cstdio>
#include<cstring>
using  namespace  std;
struct  trie
{
    int  a[26],f,v;
}tr[1100000];int  trlen,gtk[210],n;
char  st[1100000];
void  add(int  id)//添加
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'a';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];tr[root].v++;
    }
    gtk[id]=root;//记录最后一个节点。
}
int  list[1100000],head,tail;
void  bfs()
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)
            {
                tr[y].f=p;
                list[tail++]=y;
            }
            else  y=p;
        }
    }
    //AC自动机匹配。
    for(int  i=tail-1;i>=1;i--)tr[tr[list[i]].f].v+=tr[list[i]].v;//添加。
}
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add(i);//添加
    }
    bfs();//AC自动机
    for(int  i=1;i<=n;i++)printf("%d\n",tr[gtk[i]].v);//输出
    return  0;
}

练习四

用AC机额外记录Trie中每个节点所形成的字符串能够包含那些节点,用二进制储存,然后BFS一遍,最先凑够二进制的字符串就是合格的字符串。

由于要求最小,所以字符串中所有子串都对应在Trie上(简单来说就是BFS是正确的)。

#include<cstdio>
#include<cstring>
#include<cstdlib>
using  namespace  std;
struct  trie
{
    int  a[26],f,v;
}tr[1100];int  trlen,n;//字典树
char  st[110];
void  add(int  id)
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'A';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v|=(1<<(id-1));
}
int  list[2100],head,tail;
void  bfs()
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)tr[y].f=p,tr[y].v|=tr[p].v,list[tail++]=y;//更新。
            else  y=p;
        }
    }
}
struct  node
{
    char  c;
    int  next,sta,id;
}lis[2100000];bool  bol[1100][5100];
char  ans[2100000];
void  find()
{
    head=0;tail=1;//循环利用
    while(head!=tail)
    {
        node  x=lis[head++];//取队首
        for(int  i=0;i<=25;i++)
        {
            if(tr[x.id].a[i])
            {
                node  y;y.id=tr[x.id].a[i];//继续往下走
                if(!bol[y.id][tr[y.id].v|x.sta])//判重
                {
                    bol[y.id][tr[y.id].v|x.sta]=true;
                    y.c=i+'A';y.sta=tr[y.id].v|x.sta;y.next=head-1;//更新
                    lis[tail++]=y;
                    if(y.sta==(1<<(n))-1)//找到了
                    {
                        int  len=0;
                        for(int  j=tail-1;j;j=lis[j].next)ans[++len]=lis[j].c;
                        for(int  j=len;j>=1;j--)printf("%c",ans[j]);
                        printf("\n");
                        exit(0);//直接退出。
                    }
                }
            }
        }
    }
}
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add(i);
    }
    bfs();//AC机
    find();//BFS
    return  0;
}

练习五

这道题,首先,我们不能经过危险节点,危险节点就是不断跳fail能找到一个是一个字符串结尾的节点的节点。

然后DFS一遍找环,两个BOOL数组,代表是否走过与当前是否在栈里面,当我们DFS找到了在栈里面的节点,就代表找到了,退出。

注意:这里找到了如果不再栈内的话但是走过的话,不算找到,我就被坑了QAQ。

一个错误的例子:
在这里插入图片描述

代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
using  namespace  std;
struct  trie
{
    int  a[26],f,v;
}tr[1100];int  trlen,n;//字典树
char  st[110];
void  add(int  id)
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'A';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v|=(1<<(id-1));
}
int  list[2100],head,tail;
void  bfs()
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)tr[y].f=p,tr[y].v|=tr[p].v,list[tail++]=y;//更新。
            else  y=p;
        }
    }
}
struct  node
{
    char  c;
    int  next,sta,id;
}lis[2100000];bool  bol[1100][5100];
char  ans[2100000];
void  find()
{
    head=0;tail=1;//循环利用
    while(head!=tail)
    {
        node  x=lis[head++];//取队首
        for(int  i=0;i<=25;i++)
        {
            if(tr[x.id].a[i])
            {
                node  y;y.id=tr[x.id].a[i];//继续往下走
                if(!bol[y.id][tr[y.id].v|x.sta])//判重
                {
                    bol[y.id][tr[y.id].v|x.sta]=true;
                    y.c=i+'A';y.sta=tr[y.id].v|x.sta;y.next=head-1;//更新
                    lis[tail++]=y;
                    if(y.sta==(1<<(n))-1)//找到了
                    {
                        int  len=0;
                        for(int  j=tail-1;j;j=lis[j].next)ans[++len]=lis[j].c;
                        for(int  j=len;j>=1;j--)printf("%c",ans[j]);
                        printf("\n");
                        exit(0);//直接退出。
                    }
                }
            }
        }
    }
}
int  main()
{
    scanf("%d",&n);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add(i);
    }
    bfs();//AC机
    find();//BFS
    return  0;
}

练习六

这道题,先建AC自动机的图,然后DP,\(f[i][j]\)代表在字典图中最后一位在\(i\)号节点所建的最长的字符串的数量(不包含一个危险节点,定义与上一道题一样),然后DP转移,用容斥原理容易知道,只要总方案数减去没有包含一个模式串的方案数就行了。

#include<cstdio>
#include<cstring>
#define  MOD  10007
using  namespace  std;
struct  trie
{
    int  a[26],f;
    bool  v;
}tr[61000];int  trlen,n,m;//字典树
char  st[110];
void  add()
{
    int  len=strlen(st+1),root=0;
    for(int  i=1;i<=len;i++)
    {
        int  k=st[i]-'A';
        if(!tr[root].a[k])tr[root].a[k]=++trlen;
        root=tr[root].a[k];
    }
    tr[root].v=1;
}//添加
int  list[61000],head,tail;
void  bfs()
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)tr[y].f=p,tr[y].v|=tr[p].v,list[tail++]=y;//找到所有的危险节点
            else  y=p;
        }
    }
}//构建AC自动机
int  dp[61000][110];
int  main()
{
    scanf("%d%d",&n,&m);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",st+1);
        add();
    }
    bfs();
    dp[0][0]=1;//初始化
    for(int  k=1;k<=m;k++)
    {
        for(int  i=0;i<=trlen;i++)
        {
            for(int  j=0;j<=25;j++)
            {
                int  y=tr[i].a[j];
                if(!tr[y].v)dp[y][k]+=dp[i][k-1],dp[y][k]%=MOD;
            }
        }
    }//DP方程
    int  ans1=1;
    for(int  i=1;i<=m;i++)ans1*=26,ans1%=MOD;
    int  ans2=0;
    for(int  i=0;i<=trlen;i++)ans2+=dp[i][m],ans2%=MOD;
    printf("%d\n",(ans1-ans2+MOD)%MOD);//统计与相减
    return  0;
}

练习七,自行添加

暴力每个串匹配,当y的字符串上的某个节点能够直接或间接调到x字符串的结尾,就代表可以,答案++,当然TLE也十分的多QAQ。

暴力建个桶,同时离线将每个询问按y排序,然后每次跳fail的时候,问一下当前节点是否在桶里面,但是空间与时间都很棒棒!

注:一个小优化,其实可以不跳fail,可以直接跳last的,没试过,理论上可以。

那么,我们继续考虑,跳fail?我们发现一个节点的fail只会指向层数比自己低的节点,同时fail只有一个?我们可以拿trie的节点重构一棵树,当树上yy是xx的儿子,仅当yy的fail指向xx。

然后我们遍历一遍字典树,将他在重构的树中的点权设为1,然后当遇到一个字符串的结尾节点,就统计以当前字符串编号为y的所有询问中x的字符串的结尾节点在重构树中的子树权值和。

当然,还不够快,我们可以发现子树的DFS的编号是连续的,然后我们可以处理出重构树中的每个节点的DFS的编号与子树中DFS编号最大的编号是哪个,用树状数组维护每个编号的权值,统计子树和。

还是会T三个点。

然后我们可以在建Trie时还可以有一些优化,统计一下空间,每次加一个字符,由于Trie的特性,顶多加一个节点,那么Trie中的节点数最多为100000。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  210000
#define  NN  110000
using  namespace  std;
struct  trie
{
    int  a[26],f,v,fa;//fa就是father,主要用来判断他是原本字典树的还是后来字典图的一种方法
}tr[N];int  trlen,cnt,trss[N];
int  list[N],head,tail;
void  bfs()
{
    head=tail=1;
    for(int  i=0;i<=25;i++)
    {
        if(tr[0].a[i])list[tail++]=tr[0].a[i];
    }
    while(head!=tail)
    {
        int  x=list[head++];
        for(int  i=0;i<=25;i++)
        {
            int  &y=tr[x].a[i],p=tr[tr[x].f].a[i];
            if(y)
            {
                tr[y].f=p;
                list[tail++]=y;
            }
            else  y=p;
        }
    }
}
//AC机模版
struct  node
{
    int  y,next;
}a[N];int  last[N],alen;
inline  void  ins(int  x,int  y)
{
    alen++;
    a[alen].y=y;a[alen].next=last[x];last[x]=alen;
}
//边目录,重构树
struct  answers
{
    int  x,y,id;
}q[NN];/*离线操作*/int  n,m,bk[NN]/*答案*/,ql[NN],qr[NN];
int  dfn[NN]/*当前节点的字典序*/,low[NN]/*当前子树最大的DFS序*/,times/*时间戳*/;
inline  bool  cmp(answers  x,answers  y){return  x.y<y.y;}
inline  void  dfs(int  x)
{
    dfn[x]=++times;
    for(int  k=last[x];k;k=a[k].next)dfs(a[k].y);
    low[x]=times;
}
//处理字典序。
int  bit[N];
inline  int  lowbit(int  x){return  x&(-x);}
inline  void  change(int  x,int  p){while(x<=times)bit[x]+=p,x+=lowbit(x);}
inline  int  getsum(int  x)
{
    int  ans=0;
    while(x)ans+=bit[x],x-=lowbit(x);
    return  ans;
}
//树状数组  
void  find(int  x)//遍历字典树
{
    change(dfn[x],1);
    if(tr[x].v  &&  ql[tr[x].v]!=0)
    {
        int  y=tr[x].v;
        for(int  i=ql[y];i<=qr[y];i++)//遍历y是的tr[x].v的询问
        {
            bk[q[i].id]+=getsum(low[trss[q[i].x]])-getsum(dfn[trss[q[i].x]]-1);//统计
        }
    }
    for(int  i=0;i<=25;i++)
    {
        if(tr[x].a[i]  &&  tr[tr[x].a[i]].fa==x)find(tr[x].a[i]);//继续遍历
    }
    change(dfn[x],-1);
}
char  st[NN];
int  main()
{
    scanf("%s",st+1);m=strlen(st+1);
    int  root=0;
    for(int  i=1;i<=m;i++)
    {
        if(st[i]=='B')root=tr[root].fa;
        else  if(st[i]=='P')tr[root].v=++cnt,trss[cnt]=root;
        else
        {
            int  k=st[i]-'a';
            if(!tr[root].a[k])tr[root].a[k]=++trlen,tr[trlen].fa=root;
            root=tr[root].a[k];
        }
    }//省去了添加中寻找的时间,直接在主函数中快速解决,快了3000ms!
    scanf("%d",&n);
    for(int  i=1;i<=n;i++){scanf("%d%d",&q[i].x,&q[i].y);q[i].id=i;}//离线
    sort(q+1,q+n+1,cmp);//排序
    n++;
    for(int  i=1;i<=n;i++)
    {
        if(q[i].y!=q[i-1].y)ql[q[i].y]=i,qr[q[i-1].y]=i-1;//为找答案做准备
    }
    n--;ql[0]=qr[0]=0;
    bfs();//AC机
    for(int  i=1;i<=trlen;i++)ins(tr[i].f,i);//重构树
    dfs(0);//字典序。
    find(0);//寻找
    for(int  i=1;i<=n;i++)printf("%d\n",bk[i]);//输出
    return  0;
}

AC机也完成了,一步一个脚印,耶!

转载于:https://www.cnblogs.com/zhangjianjunab/p/9986015.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值