把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【CF587F】Duff is Mad(AC自动机+根号分治)

点此看题面

  • 给定\(n\)个字符串,\(q\)次询问,每次求\(s_{l\sim r}\)\(s_k\)中的出现次数总和。
  • \(n,q,\sum|s|\le10^5\)

\(AC\)自动机+根号分治

由于询问的若干串之间不相干,显然可以把询问拆成用\(s_{1\sim r}\)的答案减去\(s_{1,l-1}\)的答案,然后就变成了每次询问前\(i\)个串在\(s_k\)中的出现次数。

而众所周知,在\(AC\)自动机上,子串是前缀的后缀,前缀就是根到\(s_k\)路径上的每个节点,后缀就是一个节点的所有祖先。反过来也可以看作是根到\(s_k\)路径上的点在询问串对应的子树内。

考虑\(|s_k|\),把情况分成小于等于\(\sqrt N\)和大于\(\sqrt N\)两类。

如果\(|s_k|\le\sqrt N\),我们直接枚举把询问的\(s_k\)扔到\(l-1\)\(r\)两个端点上,然后只要枚举每个串在\(fail\)树上给子树打标记(转化成\(dfs\)序列后用树状数组),然后处理对应端点上的询问求到\(s_k\)路径上的所有点的标记和即可。

如果\(|s_k|>\sqrt N\),由于这样的串不超过\(\sqrt N\)个,我们把询问扔给\(s_k\),对于每个这样的\(s_k\)分别求解答案。具体地,我们只要给到\(s_k\)路径上的所有点打上标记,枚举每个串求出子树内的标记和,然后前缀和+差分即可。

注意到这里的树状数组可以用分块替代消去\(log\),但实际上没啥意义,估计树状数组小常数跑得更快。

代码:\(O(n\sqrt nlogn)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define LL long long
using namespace std;
int n,Nt=1,sz,id[N+5],bg[N+5],l[N+5];LL v[N+5],ans[N+5];char s[N+5];
struct Q {int p,x,y;I Q(CI i=0,CI a=0,CI b=0):p(i),x(a),y(b){}};vector<Q> V[N+5],G[N+5];vector<Q>::iterator it;
namespace AC//AC自动机
{
	#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
	struct node {int F,S[30];}O[N+5];
	int d,dI[N+5],dO[N+5],ee,lnk[N+5];struct edge {int to,nxt;}e[N+5];
	I void Init(CI x=1) {dI[x]=++d;for(RI i=lnk[x];i;i=e[i].nxt) Init(e[i].to);dO[x]=d;}//处理dfs序
	struct TreeArray
	{
		int a[N+5];I void U(RI x,CI v) {W(x<=Nt) a[x]+=v,x+=x&-x;}//单点修改/后缀修改
		I int Q(RI x,RI t=0) {W(x) t+=a[x],x-=x&-x;return t;}//前缀询问/单点询问
	}A;
	I int Ins(char* s,CI l)//插入一个串
	{
		RI x=1;for(RI i=1,t;i<=l;++i) !O[x].S[t=s[i]&31]&&(O[x].S[t]=++Nt),x=O[x].S[t];return x;//返回节点编号
	}
	int q[N+5];I void Build()//建AC自动机
	{
		RI i,k,H=1,T=0;for(i=1;i<=26;++i) (O[1].S[i]?O[q[++T]=O[1].S[i]].F:O[1].S[i])=1;
		W(H<=T) for(k=q[H++],i=1;i<=26;++i) (O[k].S[i]?O[q[++T]=O[k].S[i]].F:O[k].S[i])=O[O[k].F].S[i];
		for(i=2;i<=Nt;++i) add(O[i].F,i);Init();//建fail树
	}
	I void P(char* s,CI l,CI v) {for(RI i=1,x=1;i<=l;++i) x=O[x].S[s[i]&31],A.U(dI[x],v);}//给路径上所有点打标记
	I int G(CI x) {return A.Q(dO[x])-A.Q(dI[x]-1);}//询问子树标记和
	I void U(CI x) {A.U(dI[x],1),A.U(dO[x]+1,-1);}//给子树打标记
	I LL Q(char* s,CI l) {LL t=0;for(RI i=1,x=1;i<=l;++i) x=O[x].S[s[i]&31],t+=A.Q(dI[x]);return t;}//求路径上所有点标记和
}
int main()
{
	RI Qt,i,j;for(scanf("%d%d",&n,&Qt),i=1;i<=n;++i)
		bg[i]=bg[i-1]+l[i-1],scanf("%s",s+bg[i]+1),id[i]=AC::Ins(s+bg[i],l[i]=strlen(s+bg[i]+1));
	RI x,y,k;for(sz=sqrt(n),i=1;i<=Qt;++i) scanf("%d%d%d",&x,&y,&k),
		l[k]<=sz?(V[x-1].push_back(Q(i,k,-1)),V[y].push_back(Q(i,k,1))):G[k].push_back(Q(i,x,y));//根号分治
	for(AC::Build(),i=1;i<=n;++i) if(!G[i].empty())//对于长度超过sqrt(n)的串
	{
		for(AC::P(s+bg[i],l[i],1),j=1;j<=n;++j) v[j]=v[j-1]+AC::G(id[j]);//前缀和
		for(it=G[i].begin();it!=G[i].end();++it) ans[it->p]=v[it->y]-v[it->x-1];AC::P(s+bg[i],l[i],-1);//差分回应询问
	}
	for(i=1;i<=n;++i)//对于长度不超过sqrt(n)的串
	{
		for(AC::U(id[i]),it=V[i].begin();it!=V[i].end();++it) ans[it->p]+=it->y*AC::Q(s+bg[it->x],l[it->x]);//加入每个前缀,枚举端点上的询问
	}
	for(i=1;i<=Qt;++i) printf("%lld\n",ans[i]);return 0;
}
posted @ 2021-04-08 08:07  TheLostWeak  阅读(112)  评论(0编辑  收藏  举报