[NOI2011]阿狸的打字机——AC自动机之fail树的利用

Description

 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有28个按键,分别印有26个小写英文字母和'B'、'P'两个字母。

经阿狸研究发现,这个打字机是这样工作的:

l 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。

l 按一下印有'B'的按键,打字机凹槽中最后一个字母会消失。

l 按一下印有'P'的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。

例如,阿狸输入aPaPBbP,纸上被打印的字符如下:

a

aa

ab

我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数(x,y)(其中1≤x,y≤n),打字机会显示第x个打印的字符串在第y个打印的字符串中出现了多少次。

阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?

Input

 输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。

第二行包含一个整数m,表示询问个数。

接下来m行描述所有由小键盘输入的询问。其中第i行包含两个整数x, y,表示第i个询问为(x, y)。

Input

 输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。

第二行包含一个整数m,表示询问个数。

接下来m行描述所有由小键盘输入的询问。其中第i行包含两个整数x, y,表示第i个询问为(x, y)。

Output

 输出m行,其中第i行包含一个整数,表示第i个询问的答案。

Sample Input

aPaPBbP
3
1 2
1 3
2 3

Sample Output

2
1
0

HINT

 1<=N<=10^5

1<=M<=10^5
输入总长<=10^5

Solution

是一道能让我们深刻理解AC自动机的好题。

输入很有意思。不是直接给n个字符串,而是给了一个打字的序列。

发现,如果暴力找所有的字符串的话,那么会n^2 爆炸

B,P有点意思。而且还是可以累积的。

所以,可以直接建出来一棵trie树。B就是返回father,打一个字母就是找一个son,P就是在这个点打标记。

 

怎么找?

暴力kmp匹配n^2

我们有多个模式串,肯定要想一想AC自动机

简化问题,假设一个主串T,多次询问一个S在T中出现次数?

trie毕竟是一个前缀集合,而S在T的出现位置,只能是若干个T[1...i]前缀的后缀。

如果我们走一遍T,把路径上点权++,代表这个点是一个T的前缀。

怎么判断S在某个前缀中出现过?

反过来,如果一个前缀T[1...i]中有S,那么从i不断跳fail指针,必然有一次会跳到S的结尾处。

而,AC自动机的fail是一棵树,如果按照fail的方向,是一棵内向树。

在fail树中,如果i是S结尾处的子树中的节点,那么意味着i跳fail可以到S结尾处。

所以,可以用一个树状数组维护dfn序,就可以查询S结尾处子树内权值和。就是出现次数。

 

如果主串T稍微动一下(末尾添加字符,删除末尾字符),也可以做到。直接把点权加上、删去即可。

而这个主串变动其实是在trie上dfs

每次dfs移动显然不划算。

所以离线询问,挂在所有T的末尾。

如果dfs到Ti的末尾,回答所有询问。即Sj的fail树子树的权值和。

 

代码:

 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100000+5;
int n,m;
int sum;
struct node{
    int sub,key;
    int ans;
}que[N];
//trie&&AC machine
int ch[N][26];
bool has[N][26];
int tot;
int fail[N];
int fa[N];
vector<int>mem[N];
vector<int>exi[N];
int on[N];
char s[N];
void pre(){
    int now=0;
    fa[0]=0;
    for(int i=1;i<=n;i++){
        if(s[i]=='P'){
            sum++;
            on[sum]=now;
            exi[now].push_back(sum);
        }
        else if(s[i]=='B'){
            now=fa[now];
        }
        else{
            int x=s[i]-'a';
            if(!ch[now][x]) has[now][x]=1,ch[now][x]=++tot,fa[ch[now][x]]=now;
            now=ch[now][x];
        }
    }
}
//fail tree
struct edge{
    int nxt,to;
}e[2*N];
int hd[N],cnt;
void add(int x,int y){
    e[++cnt].nxt=hd[x];
    e[cnt].to=y;
    hd[x]=cnt;
}
int dfn[N],dfn2[N];
int df;
void dfs(int x,int fa){
    dfn[x]=++df;
    for(int i=hd[x];i;i=e[i].nxt){
        int y=e[i].to;
        if(y==fa)continue;
        dfs(y,x);
    }
    dfn2[x]=df;
}
void build(){
    queue<int>q;
    for(int i=0;i<=25;i++){
        if(has[0][i]) fail[ch[0][i]]=0,add(0,ch[0][i]),q.push(ch[0][i]);
    }
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=0;i<=25;i++){
            if(has[x][i]){
                fail[ch[x][i]]=ch[fail[x]][i];
                add(ch[fail[x]][i],ch[x][i]);
                q.push(ch[x][i]);
            }
            else{
                ch[x][i]=ch[fail[x]][i];
            }
        }
    }
}
//array tree
int f[N];
void upda(int x,int c){
    for(;x<=n;x+=x&(-x)) f[x]+=c;
}
int query(int x){
    int ret=0;for(;x;x-=x&(-x)) ret+=f[x];return ret;
}
//ans
void sol(int x,int fa){
    upda(dfn[x],1);
    for(int i=0;i<mem[x].size();i++){
        int son=on[que[mem[x][i]].sub];
        int kk=query(dfn2[son])-query(dfn[son]-1);
        que[mem[x][i]].ans=kk;
    }
    for(int i=0;i<=25;i++){
        if(has[x][i]){
            sol(ch[x][i],x);
        }
    }
    upda(dfn[x],-1);
}
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    pre();
    build();
    dfs(0,0);//warning root is 0
    scanf("%d",&m);
    int x,y;
    for(int i=1;i<=m;i++){
        scanf("%d%d",&que[i].sub,&que[i].key);
        que[i].ans=0;
        mem[on[que[i].key]].push_back(i);
    }
    sol(0,0);
    for(int i=1;i<=m;i++){
        printf("%d\n",que[i].ans);
    }
    return 0;
}

 

 


 

upda:2019.3.27:

直接暴力fail树上线段树合并,就可以在线辣 

 

posted @ 2018-10-08 22:09  *Miracle*  阅读(212)  评论(0编辑  收藏  举报