题面
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