【XSY2499】字符串(AC自动机+树状数组)

题面

Description

UPD:本题字符集为全体小写字母

在这里插入图片描述

Input

在这里插入图片描述

Output

在这里插入图片描述

Sample Input

5
1 abc
3 abcabc
0 abc 
3 aba
1 abababc

Sample Output

2
2

HINT

在这里插入图片描述

题解

这个“强制在线”好假……

法一:

我们如果用暴力做法,就是在\(1\)\(2\)操作时将字符串强制插入\(AC\)自动机修改对应节点的\(val\)值,然后在\(3\)操作时先对自动机做一遍\(get\_fail\),再用普通方法\(query\)查询每个字符串在\(S\)中出现的次数。

但是我们发现这样会死循环,原因是在第一次\(getfail\)时,会有如下操作:

t[u].ch[i]=t[t[u].fail].ch[i]

这样有可能会让\(child\)树形成一个环,也就是说这次\(get\_fail\)完后再插入时跑\(child\)树可能会死循环。

因为插入必须按原来的\(child\)树跑,所以我们就想如何维护如何区分是原\(child\)树还是\(get\_fail\)后的\(child\)树。我们有注意到如果将\(child\)树分层,每个节点的深度为\(d[i]\),那么必有\(d[fail[u]]<=d[u]\),因为\(fail[u]\)所代表的字符串是\(u\)所代表的字符串的一个后缀,所以\(d[ch[fail[u]]]<=d[u]<d[ch[u]]\),所以如果新\(child\)树里某个节点的儿子的\(d\)比它小,这就不是旧\(child\)树中所含有的儿子,反之,如果新\(child\)树里某个节点的儿子的\(d\)比它大,这就是旧\(child\)树中所含有的儿子。

然后就可以处理这个问题了,之后就按上述方法做就行了(但这个算法时间复杂度比较高,\(3000ms\)情况下需要卡常)。

做法是同学的,我自己没写代码

法二:

我们发现,这里的“强制在线”仅仅是针对操作类型的,而不是针对字符串的,说明该用到的字符串还是要用到。

那么我们就可以将所有的读入的字符串先插入到\(AC\)自动机里(不管是询问还是修改,因为你也不知道),再做一遍\(get\_fail\),后面直接按常规方法求解就行了。

但我们发现当数据达到最大时,这样的时间复杂度是不行的,所以我们考虑如何维护“修改”和“常规方法求解”。

如何维护呢?我们现将\(fail\)树建出来:

for(int i=1;i<=tot;i++)
        add_edge(t[i].fail,i);

那么对于原来的\(query\)代码:

long long query(string s)
{
    long long ans=0;
    int u=0,len=s.size();
    for(int i=0;i<len;i++)
    {
        int v=s[i]-'a';
        u=t[u].ch[v];
        for(int now=u;now;now=t[now].fail)ans+=a[now];//不断跳fail的过程
    }
    return ans;
}

我们发现不断跳\(fail\)\(ans\)的过程就是在\(fail\)树上从\(u\)到根加上每个点的\(val\)值的过程。

然后对于修改,我们发现就是将\(S\)\(fail\)树中的对应点的子树都将\(val\)值加\(1\)或减\(1\)

以上操作都可以用很多数据结构维护,我选了码量少时间复杂度低的树状数组。

至于如何维护,见注释。

下面是详细代码:

#include<bits/stdc++.h>
 
#define N 1000010
 
using namespace std;
 
struct Trie
{
    int ch[26],fail,val;
}t[N];
 
int m,tot,cnt,idx,opt[N],ed[N],mask,l[N],head[N],to[N<<1],nxt[N<<1],a[N],dfn[N],size[N],c[N];
char s[N];
 
void adde(int u,int v)//fail树建边
{
    to[++cnt]=v;
    nxt[cnt]=head[u];
    head[u]=cnt;
}
 
int lowbit(int x)//树状数组部分
{
    return x&(-x);
}
 
void add(int x,int y)
{
    for(;x<=idx;x+=lowbit(x))c[x]+=y;
}
 
long long ask(int x)
{
    long long ans=0;
    for(;x;x-=lowbit(x))ans+=c[x];
    return ans;
}
 
void dfs(int u)//维护树上树状数组
{
    dfn[u]=++idx;//先记录遍历fail树的顺序
    size[u]=1;//记录子树+自己的节点个数
    for(int i=head[u];i;i=nxt[i])
    {
        dfs(to[i]);
        size[u]+=size[to[i]];
    }
    //当子树遍历完后,我们发现dfn[u]+size[u]-1就是遍历子树时最后一个节点的dfn值,且dfn[u]~dfn[u]+size[u]-1所有的值都是u及其子树某个节点的dfn值,换句话说,u及其子树每个节点的dfn值,都在dfn[u]~dfn[u]+size[u]-1内且一一对应。
}
 
void insert(int l,int r,int x,int id)//插入字符串
{
    int u=0;
    for(register int i=l;i<=r;i++)
    {
        int v=s[i]-'a';
        if(!t[u].ch[v])
            t[u].ch[v]=++tot;
        u=t[u].ch[v];
    }
    t[u].val+=x;
    ed[id]=u;
}
 
void getfail()//get_fail+fail树建边
{
    queue<int>q;
    for(register int i=0;i<26;i++)if(t[0].ch[i])q.push(t[0].ch[i]);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(register int i=0;i<26;i++)
        {
            int p=t[t[u].fail].ch[i];
            if(t[u].ch[i])
            {
                t[t[u].ch[i]].fail=p;
                q.push(t[u].ch[i]);
            }
            else t[u].ch[i]=p;
        }
    }
    for(int i=1;i<=tot;i++)
        adde(t[i].fail,i);
}
 
long long query(int l,int r)//询问
{
    long long ans=0;
    int u=0;
    for(register int i=l;i<=r;i++)
    {
        int v=s[i]-'a';
        u=t[u].ch[v];
        ans+=ask(dfn[u]);
    }
    return ans;
}
 
int main()
{   
    scanf("%d",&m);
    char str[N];
    for(register int i=1;i<=m;i++)
    {
        scanf("%d",&opt[i]);
        scanf("%s",str+1);
        l[i]=l[i-1]+strlen(str+1);
        for(int j=l[i-1]+1;j<=l[i];j++)
            s[j]=str[j-l[i-1]];
        insert(l[i-1]+1,l[i],0,i);
    }
    getfail();
    dfs(0);
    for(register int i=1;i<=m;i++)
    {
        opt[i]^=mask;
        if(opt[i]==1)
        {
            add(dfn[ed[i]],1);
            add(dfn[ed[i]]+size[ed[i]],-1);//区间树状数组修改
        }
        if(opt[i]==2)
        {
            add(dfn[ed[i]],-1);
            add(dfn[ed[i]]+size[ed[i]],1);
        }
        if(opt[i]==3)
        {
            long long ans=query(l[i-1]+1,l[i]);
            printf("%lld\n",ans);
            mask^=abs(ans);
        }
    }
    return 0;
}

总结

不要被题目的假象所迷惑,要看清题目的本质。

\(get\_fail\)\(child\)树不能直接跑。

\(fail\)树是解决\(AC\)自动机上的询问的一种好方法。

posted @ 2022-10-30 10:08  ez_lcw  阅读(17)  评论(0编辑  收藏  举报