zoukankan      html  css  js  c++  java
  • 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
    ",Ans[i]);
        return 0;
    }
  • 相关阅读:
    python3 numpy基本用法归纳总结
    MySQL 中的三中循环 while loop repeat 的基本用法
    什么是网关及网关作用
    网络扫描工具nmap
    nmap基本使用方法
    nmap脚本使用总结
    用Delphi将数据导入到Excel并控制Excel
    delphi Form属性设置 设置可实现窗体无最大化,并且不能拖大拖小(写一个WM_EXITSIZEMOVE的空函数)
    Delphi 数据类型列表
    一个队列类的实现(比delphi自带的速度快70倍)(线程安全版本)
  • 原文地址:https://www.cnblogs.com/LLTYYC/p/9692131.html
Copyright © 2011-2022 走看看