【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 1 1 2 2 2操作时将字符串强制插入 A C AC AC自动机修改对应节点的 v a l val val值,然后在 3 3 3操作时先对自动机做一遍 g e t _ f a i l get\_fail get_fail,再用普通方法 q u e r y query query查询每个字符串在 S S S中出现的次数。

但是我们发现这样会死循环,原因是在第一次 g e t f a i l getfail getfail时,会有如下操作:

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

这样有可能会让 c h i l d child child树形成一个环,也就是说这次 g e t _ f a i l get\_fail get_fail完后再插入时跑 c h i l d child child树可能会死循环。

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

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

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

法二:

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

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

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

如何维护呢?我们现将 f a i l fail fail树建出来:

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

那么对于原来的 q u e r y query 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;
}

我们发现不断跳 f a i l fail fail a n s ans ans的过程就是在 f a i l fail fail树上从 u u u到根加上每个点的 v a l val val值的过程。

然后对于修改,我们发现就是将 S S S f a i l fail fail树中的对应点的子树都将 v a l val val值加 1 1 1或减 1 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;
}

总结

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

g e t _ f a i l get\_fail get_fail c h i l d child child树不能直接跑。

f a i l fail fail树是解决 A C AC AC自动机上的询问的一种好方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值