P2414 [NOI2011]阿狸的打字机

传送门

先想想暴力怎么搞

搞一个AC自动机

对每个询问 x,y

把 y 暴力向下匹配

每个点都暴力跳fail

看看x出现了几次

稍微优化一波

因为有多组询问

考虑离线

可以把同一组的 y 一起来计算

还是把 y 暴力匹配

看看所有的 x 出现了几次

再来一波优化

考虑什么时候 x 的出现次数会增加

显然是在 y 的某个节点的 fail 路径上

因为每个点只有一个 fail

所以

所有的 fail 构成了一颗树

如果把 fail 看成无向边,根节点为自动机的根节点

那就相当于问 在fail树的x的结束节点的子树中,有几个节点属于y

那么询问 x,y 就只要在AC自动机上跑到y

路过的每一个节点就把 记录值+1

然后询问 在fail树中 x 的结束节点的子树 记录值之和为多少

对于这种对子树的询问

用什么方法最好呢?

树链剖分!

为什么怎么麻烦

虽然不可能用树剖

但是可以用树剖的思想

给每个节点一个dfs序

在AC自动机上跑的时候

每经过一个节点就把该节点的dfs序的值+1

退出该节点时就-1

然后询问就像树剖的子树询问一样了

因为每次是单点修改,区间求和

所以用树状数组维护一波就好了

总结一下

把每个操作离线

把y相同的询问放在一起

dfs一波,每次找到结束标记

就把相关的询问处理

具体的实现在代码里

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
inline int read()
{
    int x=0; char ch=getchar();
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9')
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x;
}

const int N=1e5+7;
char s[N];
int fa[N];
int n,m;
int c[N][27],pd[N],fail[N],cnt,las[N];//las[i]表示第i个串的结束节点的位置
inline void build()//处理操作并构造出AC自动机
{
    scanf("%s",s);
    int u=0,len=strlen(s);
    for(int i=0;i<len;i++)
    {
        if(s[i]=='B') u=fa[u];//回到上一个位置相当于删除最后一个单词
        if(s[i]=='P') pd[u]=++n,las[n]=u;//记录一波
        if(s[i]!='B'&&s[i]!='P')
        {
            int v=s[i]-'a'+1;
            if(!c[u][v]) c[u][v]=++cnt;
            fa[c[u][v]]=u; u=c[u][v];//记录上一个位置并向下走
        }
    }
}

//以下为预处理fail并构造出fail树
queue <int> q;
int fir[N],from[N],to[N],cnt2;//存fail树
inline void Add(int a,int b)
{
    from[++cnt2]=fir[a];
    fir[a]=cnt2; to[cnt2]=b;
}//向fail树中加边
int C[N][27];//存原AC自动机的结构,因为处理fail时会把AC自动机的结构改变
//等等还要用原来的结构来dfs处理询问
void pre()
{
    for(int i=0;i<=cnt;i++)
        for(int j=1;j<=26;j++) C[i][j]=c[i][j];//拷贝一波
    for(int i=1;i<=26;i++) if(c[0][i]) q.push(c[0][i]),Add(0,c[0][i]);//从根到这些节点也有fail边
    //加边时加单向边就好了,没影响
    while(!q.empty())
    {
        int u=q.front(); q.pop();
        for(int i=1;i<=26;i++)
        {
            int v=c[u][i];
            if(!v) c[u][i]=c[fail[u]][i];
            else fail[v]=c[fail[u]][i],Add(fail[v],v),q.push(v);//预处理fail并构造出fail树,重要操作
        }
    }
}
//以上为预处理fail并构造出fail树

//第一波dfs确定dfs序
int dfn[N],sz[N],cnt3;//dfn是dfs序,sz是子树大小
void dfs1(int x)
{
    dfn[x]=++cnt3; sz[x]=1;
    for(int i=fir[x];i;i=from[i])
        dfs1(to[i]),sz[x]+=sz[to[i]];
}

//以下为树状数组
int t[N];
inline void add(int x,int v){ while(x<=cnt3) t[x]+=v,x+=x&-x; }
inline int query(int x)
{
    int res=0;
    while(x) res+=t[x],x-=x&-x;
    return res;
}
//以上为树状数组

//以下存询问
struct data
{
    int x,y,id,ans;
}d[N];
inline bool cmp(const data &a,const data &b){ return a.y<b.y; }
int l[N],r[N];//l[i]表示排序后y值为i的区间的左端点,r为右端点
//以上存询问

void dfs2(int x)
{
    add(dfn[x],1);
    if(pd[x])//如果找到结束标记
        for(int i=l[pd[x]];i<=r[pd[x]];i++)//把所有相关的询问都处理掉,显然此时只有属于y的节点有1的值
            d[i].ans=query( dfn[ las[d[i].x] ]+sz[ las[d[i].x] ]-1 )-query( dfn[ las[d[i].x] ]-1 );//像树剖一样询问,注意减1
    for(int i=1;i<=26;i++)
    {
        int v=C[x][i];//在原来的自动机上跑
        if(!v) continue;//可能后面没有节点了,不能走
        dfs2(v);
    }
    add(dfn[x],-1);//退出时值要改回来
}

int Ans[N];
int main()
{
    build();
    pre();

    //读入询问并处理l,r
    cin>>m;
    for(int i=1;i<=m;i++)
        d[i].x=read(),d[i].y=read(),d[i].id=i;
    sort(d+1,d+m+1,cmp);
    l[d[1].y]=1;
    for(int i=2;i<=m;i++)
        if(d[i].y!=d[i-1].y)
        {
            r[d[i-1].y]=i-1;
            l[d[i].y]=i;
        }
    r[d[m].y]=m;

    dfs1(0);//确定dfs序
    dfs2(0);//dfs处理询问

    for(int i=1;i<=m;i++)
        Ans[d[i].id]=d[i].ans;//按原来的顺序把答案放到答案数组里
    for(int i=1;i<=m;i++) printf("%d\n",Ans[i]);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/LLTYYC/p/9692131.html