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

【LOJ6031】「雅礼集训 2017 Day1」字符串(后缀自动机+设阈值)

点此看题面

  • 给定一个长度为\(n\)的字符串\(s\)\(m\)个区间。
  • \(q\)次询问,每次给定一个长度为\(k\)的字符串\(w_i\)和两个数\(l_i,r_i\),求第\(l_i\sim r_i\)个区间在\(w_i\)中对应的子串在\(s\)中出现次数之和。
  • \(n,m,q\times k\le10^5\)

根号分治/分类讨论

首先对于这种问题容易想到对于给定串\(s\)建一个后缀自动机。

然后发现由于题目中给的是\(q\times k\)的限制,感觉很难给出一个通解。

因此我们考虑设阈值,其实这道题中也就是根据\(q,k\)的大小关系分类讨论。

每块分类讨论中会给出具体的复杂度,最终代码的复杂度表示就笼统地将\(n,m,qk\)视为同阶的\(N\)了。

\(k\le q\)的暴力查询

考虑我们枚举\(w\)中的每一个左端点\(i\),然后每次右移一位右端点\(j\)都相当于是在后缀自动机上向下走了一步。

\([i,j]\)子串在\(s\)中的出现次数实际上就是当前节点在\(parent\)树上的子树大小(这显然可以预处理出来)。

而我们一开始就可以对于每个\([i,j]\)\(vector\)存下等于它的区间编号,那么每次只要调用\(lower\_bound\)\(upper\_bound\)便能求出有多少为\([i,j]\)的区间在询问的\([l,r]\)中。

这一部分的复杂度应该是\(O(k^2qlogm)\)的。

\(k>q\)的倍增求解

考虑我们先对于\(w\)中的每个位置,求出以它为右端点能得到的最长后缀在后缀自动机中的对应节点及其长度,这只要在后缀自动机上走一遍就好了,和\(AC\)自动机完全一样。(可以看代码,应该非常显然)

然后枚举\(l\sim r\)的区间\([L_i,R_i]\),先判断如果\(R_i\)对应的最长后缀长度小于\(R_i-L_i+1\),说明这个子串压根没出现过。

否则,我们在\(parent\)树上倍增上跳,找到深度最小的长度大于等于\(R_i-L_i+1\)的节点,则这个节点的子树大小就是子串\([L_i,R_i]\)\(s\)中出现的次数。

这一部分的复杂度应该是\(O(qmlogn)\)的。

代码:\(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 SN 350
#define LN 20
#define LL long long
using namespace std;
int n,m,k,Qt;char s[N+5];struct Q {int l,r;}q[N+5];
class SuffixAutomation//后缀自动机
{
	private:
		int Nt,lst,cur,ct[N+5],q[N<<1];struct node {int Sz,C,L,F[LN+5],S[30];}O[N<<1];
		I void MakeFa(CI x,CI f)//让x的父节点变成f
		{
			O[x].F[0]=f;for(RI i=1;i<=LN;++i) O[x].F[i]=O[O[x].F[i-1]].F[i-1];//预处理倍增数组
		}
	public:
		I SuffixAutomation() {Nt=1;}I void Init() {lst=1;}I void Ins(CI x)//插入字符
		{
			RI p=lst,o=lst=++Nt;O[o].L=O[p].L+1,O[o].Sz=1;
			W(p&&!O[p].S[x]) O[p].S[x]=o,p=O[p].F[0];if(!p) return MakeFa(o,1);
			RI q=O[p].S[x];if(O[p].L+1==O[q].L) return MakeFa(o,q);
			RI k=++Nt;(O[k]=O[q]).L=O[p].L+1,O[k].Sz=0,MakeFa(o,k),MakeFa(q,k);
			W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F[0];
		}
		I void Calc(CI n)//统计子树大小(基排,或DFS也可)
		{
			RI i;for(i=1;i<=Nt;++i) ++ct[O[i].L];for(i=1;i<=n;++i) ct[i]+=ct[i-1];
			for(i=1;i<=Nt;++i) q[ct[O[i].L]--]=i;for(i=Nt;i^1;--i) O[O[q[i]].F[0]].Sz+=O[q[i]].Sz;
		}
		I void Start() {cur=1;}I int Go(CI x) {return O[cur=O[cur].S[x]].Sz;}//k≤q时在后缀自动机上走路
		I void Add(CI id,CI x)//k>q预处理时加入一个新字符
		{
			!(q[id]=q[id-1])&&(q[id]=1),ct[id]=ct[id-1];//从上个字符继承信息
			W(q[id]&&!O[q[id]].S[x]) ct[id]=O[q[id]=O[q[id]].F[0]].L;//如果没有这个儿子就不断上跳父节点
			(q[id]=O[q[id]].S[x])?++ct[id]:ct[id]=0;//维护最大长度
		}
		I int Get(CI id,CI x)//k>q时倍增求解
		{
			if(ct[id]<x) return 0;RI p=q[id];//如果长度本就不足直接返回0
			for(RI i=LN;~i;--i) O[O[p].F[i]].L>=x&&(p=O[p].F[i]);return O[p].Sz;//倍增上跳
		}
}S;
vector<int> g[SN+5][SN+5];I void SolveK()//k≤q
{
	RI i,j,l,r,tmp;LL t;for(i=1;i<=m;++i) g[q[i].l][q[i].r].push_back(i);W(Qt--)//存下每种询问的区间编号
	{
		#define G g[i][j].begin(),g[i][j].end()
		for(scanf("%s%d%d",s+1,&l,&r),++l,++r,t=0,i=1;i<=k;++i)//枚举左端点
			for(S.Start(),j=i;j<=k;++j) t+=1LL*S.Go(s[j]&31)*(upper_bound(G,r)-lower_bound(G,l));//移动右端点
		printf("%lld\n",t);
	}
}
I void SolveQ()//k>q
{
	RI i,l,r;LL t;W(Qt--)
	{
		for(scanf("%s%d%d",s+1,&l,&r),++l,++r,S.Start(),i=1;i<=k;++i) S.Add(i,s[i]&31);//求出每个右端点最长后缀对应的节点及其长度
		for(t=0,i=l;i<=r;++i) t+=S.Get(q[i].r,q[i].r-q[i].l+1);printf("%lld\n",t);//每个询问在parent树上倍增
	}
}
int main()
{
	RI i;for(scanf("%d%d%d%d%s",&n,&m,&Qt,&k,s+1),S.Init(),i=1;i<=n;++i) S.Ins(s[i]&31);//建后缀自动机
	for(i=1;i<=m;++i) scanf("%d%d",&q[i].l,&q[i].r),++q[i].l,++q[i].r;//读入区间
	return S.Calc(n),k<=Qt?SolveK():SolveQ(),0;//分类讨论
}
posted @ 2020-12-29 20:21  TheLostWeak  阅读(97)  评论(0编辑  收藏  举报