首页 > 其他分享 >【XSY2499】字符串(AC自动机+树状数组)

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

时间:2022-10-30 10:26:29浏览次数:78  
标签:AC 树状 int XSY2499 字符串 child fail get 自动机

题面

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\)自动机上的询问的一种好方法。

标签:AC,树状,int,XSY2499,字符串,child,fail,get,自动机
From: https://www.cnblogs.com/ez-lcw/p/16840599.html

相关文章

  • 【USACO10JAN】Cheese Towers S 奶酪塔 (背包dp)
    一种思路奇特的做法。看到题目容易联想到背包dp,因为看上去很像。但是我们并不知道上面有没有大奶酪。所以我们不妨倒过来看,从上往下加奶酪。设\(dp(i,1/0)\)表示当前......
  • NISACTF2022--join-us(join using 无列名注入)
    join-us--join-using无列名查询访问登录页面,看到一个catugetflag输入框试试sql注入有报错信息  黑名单byupdatexmldatabaseunioncolumns=substr......
  • AcWing 1209. 带分数
    题目条件:枚举全排列,是9个数a,b,c的位数都还不知道枚举a,b,c的位数,枚举a和b的位数,c=9-a-b判断等式是否成立//暴力dfs#include<iostream>#include<cstrin......
  • Oracle问题记录
    概述只稍微熟悉MySQL,但是不可避免会使用Oracle,此文记录Oracle使用问题。问题获取版本号​​SELECTversionFROMPRODUCT_COMPONENT_VERSIONWHEREproductLIKE'OracleDa......
  • logback笔记
    概述配置错误日志发送到指定邮箱<appendername="EMAIL"class="com.ppdai.logclient.logback.MailAppender"><evaluatorclass="com.ppdai.logback.SMTPFrequencyE......
  • Anaconda navigator 打不开?打开没反应问题?
    ​​Anaconda​​navigator打不开的解决方案当你想打开Anacondanavigator的时候,出现下图所示,anaconda已经在运行,但是你在任务管理器里却无法查看的,此时我们可以选择用CMD......
  • React父组件使用子组件数据
    TypeScript语言。子组件​​​TopBar.tsx​​:importReact,{createContext}from'react';import{FormInstance}from"antd/lib/form";constformRef=React.createRe......
  • OpenGL ES EGL eglCreateWindowSurface
    一.EGL前言二.EGL绘制流程简介三.eglCreateWindowSurface函数简介1.eglCreateWindowSurface函数2.EGLSurface分类四.eglCreateWindowSurface函数使用五.......
  • 【已验证】使用composer 出现Could not find a matching version of package xxx
    今天使用composer安装一个包,开始我指定了版本,报错但是我后来,没有指定版本,还是报错??去百度查了下,出现这个问题,有两个原因:你设置的composer的原有问题(我的源我都用了好......
  • acwing第75场周赛
    这次题比较水,但是还是没能ak,自己小结一下吧第一道题就是自己枚举相加就行第二道题是一个多关键字排序,wa了几次,是因为优先级有两个是相同的需要特判一下,然后可以把字符转......