CF710F String Set Queries 题解

Description

维护一个字符串集合,支持以下操作:

  • 加字符串

  • 删字符串

  • 查询集合中的所有字符串在模式串中出现次数之和

操作数 \(n\leq 3\times 10^5\),字符串长度和 \(\sum |s|\leq 3\times 10^5\)

本题强制在线。

Solution

先考虑最暴力怎么做:建棵 \(AC\) 自动机,每次加(删)字符串的时候暴力重构,查询的时候直接匹配即可。

但这样做复杂度是 \(O(n\sum |S|)\) 的,肯定会炸。

思考瓶颈在哪里——每次都重构自动机太费时间。

那我们的优化思路就是减少每个字符串被重构的次数,大力分块当然可以,但很慢,还有一个经典想法是二进制分组。

维护 \(log\sum |S|\) 个自动机,当一个自动机与前一个自动机的字符串数量一样时合并两个自动机,这样每个字符串就最多被合并 \(log\sum |S|\) 次,达到了减少重构次数的目的。

查询的时候,把每个自动机的贡献加起来就好了。

时间复杂度 \(O(nlog\sum |S|)\)

Code

#include<bits/stdc++.h>
#define IL inline
#define RE register
#define N 300300
int n,type;
int fail[N],ch[N][26],f[N],g[N],tot;
int rt[20],size[20],top;
char s[N];
int len;

IL void insert(int rt,short k)
{
	int p=rt;
	for(RE int i=1;i<=len;++i)p=ch[p][s[i]-'a']?ch[p][s[i]-'a']:ch[p][s[i]-'a']=++tot;
	g[p]+=k;
}

std::queue<int>q;

IL void build(int rt)
{
	f[rt]=0;
	for(RE int i=0;i<26;++i)if(ch[rt][i])q.push(ch[rt][i]),fail[ch[rt][i]]=rt;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		f[u]=f[fail[u]]+g[u];
		for(RE int i=0;i<26;++i)if(ch[u][i])
		{
			q.push(ch[u][i]);
			int v=fail[u];
			for(;v&&!ch[v][i];v=fail[v]);
			fail[ch[u][i]]=!v?rt:ch[v][i];
		}
	}
}

int merge(int x,int y)
{
	if(!x||!y)return x^y;
	g[x]+=g[y];
	for(RE int i=0;i<26;++i)ch[x][i]=merge(ch[x][i],ch[y][i]);
	return x;
}

IL int match(int rt)
{
	int sum=0,p=rt;
	for(RE int i=1;i<=len;++i)
	{
		short c=s[i]-'a';
		for(;!ch[p][c]&&p^rt;p=fail[p]);
		if(ch[p][c])sum+=f[p=ch[p][c]];
	}
	return sum;
}

int main()
{
	scanf("%d",&n);
	int opt,ans;
	while(n--)
	{
		scanf("%d%s",&opt,s+1);
		len=strlen(s+1);
		if(opt^3)
		{
			rt[++top]=++tot,size[top]=1;
			insert(tot,opt<2?1:-1);
			for(;top>1&&size[top]==size[top-1];rt[top-1]=merge(rt[top-1],rt[top]),size[--top]<<=1);
			build(rt[top]);			
		}
		else
		{
			ans=0;
			for(RE int i=1;i<=top;++i)ans+=match(rt[i]);
			printf("%d\n",ans);
			fflush(stdout);
		}
	}
	return 0;
}
posted @ 2021-08-27 16:48  Nesraychan  阅读(138)  评论(0)    收藏  举报