洛谷P2414 【[NOI2011]阿狸的打字机】题解(AC自动机上fail树+主席树/普通线段树/树状数组)

前言(在博客园发布的第一篇题解):

这可是道好题啊,我用的是利用可持久化线段树在线查询具有历史版本的基于AC自动机的fail树,当然更多的大佬是用普通线段树或树状数组离线查询,蒟蒻表示不想离线,就写了个在线的,码量也不大,去掉注释可达到 3kb 以内(没比离线多多少),而且很多都是模板代码,敲起来贼快。(加 O2 可跑到 317 ms,还挺快的)。

另外,个人认为这题若不是年代久远,类似的题变多,成为套路,是有机会成为黑题的。

题目传送门

正文:

Step 1(要利用的算法):

首先,题目打印出来的字符串都是前一个字符串进行删除一些字母和添加一些字母得到的,可看成不同历史版本,可以盲猜要用到可持久化线段树(雾,我也不知道怎会又这种想法,洛谷题解区几乎都是离线查询的)。

由于涉及到多个字符串,就肯定要用 AC 自动机。

Step 2(思路及具体实现过程):

这一步就是把题目进行转换,参考了下各篇题解,发现大佬们说得都很清楚,蒟蒻综合一下。

  1. 字符串 \(a\)\(b\) 中出现 \(\Leftrightarrow\) 字符串 \(a\)\(b\) 一个前缀的后缀。
  2. 建立 trie 树后,若在根到 \(b\) 的路径上,存在一点 \(c\),使得跑 AC 自动机查询时,字符串 \(a\) 的末尾(结点 \(d\))刚刚匹配到 \(c\),就说明 \(a\)\(b\) 一个前缀的后缀,而这又 \(\Leftrightarrow\) 建立 fail 树后,\(d\)\(c\) 的祖先。
  3. 所以,我们要查找第 \(x\) 个打印的字符串(对应可持久化线段树历史版本 \(u\))在第 \(y\) 个打印的字符串(对应可持久化线段树历史版本 \(v\))中出现了多少次就 \(\Leftrightarrow\) 在可持久化线段树第 \(v\) 个历史版本,历史版本 \(u\) 对应 dfs 序结点的那棵子树中,求所有点对答案有无贡献(有贡献则给答案加 1,有无贡献可以遍历原始 trie 树,动态加点时统计)。

Step 3(Code,附详细注释,没挖坑,可以放心食用):

//算是为数不多的自己进行详细注释的代码吧
//做法:利用可持久化线段树在线查询具有历史版本的基于AC自动机的fail树。
#include<bits/stdc++.h>
#define ll long long
#define linf 0x3f3f3f3f3f3f3f3f
#define inf 0x7fffffff
#define v e[i].y
using namespace std;
inline ll read(){
    char ch=getchar();ll x=0,w=1;
    while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();return x*w;
}
inline void write(ll x){
	if(x<0)x=-x,putchar('-');
    if(x<10){putchar(48+x);return;}
    write(x/10),putchar((x+10)%10+48);
}
int n,m,ans,cnt,xx,dfc,h[300005],X[300005],s[300005],dfn[300005];
string fj;
struct node{int y,nxt;}e[1000005];//Fail树的邻接表
void add(int o1,int o2){e[cnt].nxt=h[o1],e[cnt].y=o2,h[o1]=cnt++;}
struct AC{//AC自动机
    int nxt[300005][26],nxt1[300005][26],Fail[300005],b[300005],q[1000005],pre[300005],he,ti,num;
    void Insert(string St,int L){//和AC自动机普通插入不同,要特判B和P
        int op=0;
        for(int i=0;i<L;i++){
            if(St[i]=='B')op=pre[op];//pre为前驱。
            else if(St[i]=='P')X[++xx]=op;//统计主席树各个版本的信息。
            else{
                if(!nxt[op][St[i]-'a'])nxt[op][St[i]-'a']=nxt1[op][St[i]-'a']=++num,pre[num]=op;//维护单向链表。
                op=nxt[op][St[i]-'a'];
            }
        }
    }
    void getfail(){
        for(int i=0;i<26;i++)if(nxt[0][i])q[++ti]=nxt[0][i],Fail[nxt[0][i]]=0;
        while(he<ti){
            int u=q[++he];
            for(int i=0;i<26;i++){
                if(nxt[u][i])Fail[nxt[u][i]]=nxt[Fail[u]][i],q[++ti]=nxt[u][i];
                else nxt[u][i]=nxt[Fail[u]][i];
            }
        }
    }//以上为常规操作
}A;
struct pst{//主席树
    int sum[5000005],rt[5000005],lc[5000005],rc[5000005],num;
    void Update(int &o,int P,int x,int l,int r){//不知道算是修改还是建树。
        o=++num,sum[o]=sum[P],lc[o]=lc[P],rc[o]=rc[P],sum[o]++;//这点对于答案有贡献,要在前驱基础上加1。
        if(l==r)return;
        int mid=l+r>>1;
        if(mid>=x)Update(lc[o],lc[P],x,l,mid);
        else Update(rc[o],rc[P],x,mid+1,r);
    }
    int query(int o,int l,int r,int x,int y){
        if(l>=x&&r<=y)return sum[o];
        int mid=l+r>>1,an=0;
        if(x<=mid)an+=query(lc[o],l,mid,x,y);
        if(mid<y)an+=query(rc[o],mid+1,r,x,y);
        return an;
    }//以上为常规操作
    void dfs(int x){//统计fail树的dfs序和子树大小
        dfn[x]=++dfc,s[x]=1;
        for(int i=h[x];i!=-1;i=e[i].nxt)dfs(v),s[x]+=s[v];
    }
    void dfs1(int x){//在trie树上进行操作,把每个结点添加到主席树上。
        Update(rt[x],rt[A.pre[x]],dfn[x],1,dfc);
        for(int i=0;i<26;i++)if(A.nxt1[x][i])dfs1(A.nxt1[x][i]);//由于每个结点建一次就够,所以要用最初始的nxt。
    }
    void solve(){
        A.getfail();
        for(int i=1;i<=A.num;i++)add(A.Fail[i],i);//建fail树。
        dfs(0),dfs1(0);
        int _1,_2;
        while(m--){//支持在线查询
            _1=read(),_2=read();
            write(query(rt[X[_2]],1,dfc,dfn[X[_1]],dfn[X[_1]]+s[X[_1]]-1)),putchar('\n');
        }
    }
}B;
int main(){
    memset(h,-1,sizeof(h));
    cin>>fj,A.Insert(fj,fj.size());
    m=read(),B.solve();
    return 0;
}
posted @ 2022-07-10 11:44  mcDinic  阅读(53)  评论(0)    收藏  举报