【CF666E】Forensic Examination(广义后缀自动机+线段树合并)

点此看题面

  • 给出一个字符串(S)以及(m)个字符串(T_{1sim m})
  • (q)次询问,每次询问(S[x..y])(T_{lsim r})中的哪个串中出现次数最多(多个最多则输出编号最小的)及对应的出现次数。
  • (|S|le5 imes10^5,sum|T|le5 imes10^4,qle5 imes10^5)

广义后缀自动机

虽然是比较基础的知识,但还是不得不提一下——我一直以来写的都是假的广义后缀自动机,结果因为这个东西直接调了快一个小时。

错误的写法(我原先的写法)是每次直接把(lst)调回(1),然后就不管了。

然而这道题中我们需要合并相同的节点,否则次数的统计会出现一些问题。

所以我们实际上给(lst)插入一个字符(x)时要先判断对应的后继是否存在,如果存在就不需要新建节点,而是直接把原本插入中向上跳找到存在(x)后继节点后的代码贴过来:若长度恰好比当前节点大(1)就是该点,否则新建一个长度比当前节点大(1)的点作为新后继,原后继的父节点修改为新后继。(具体实现详见代码)

线段树合并

我们记录(S)每个前缀在后缀自动机上对应的节点编号(p_i)

每次要找到子串([l,r]),只要从(p_r)开始在(parent)树上倍增上跳,找到最浅的最大长度大于等于(r-l+1)的节点。

要求这个节点在(T_{lsim r})中的出现次数,需要事先利用线段树合并预处理。

即插入(T_i)的时候在每个前缀节点的线段树上给(i)(1),并在所有串插入完之后再从下往上把当前点的线段树合并到(parent)树中父节点的线段树上。

这样一来,每个节点线段树就维护出了每个串中这个串的出现次数,而要在线段树上询问一段区间中的最大值及最早出现位置应该是非常简单的。

代码:(O(|S|log|S|))

#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 SZ 550000
#define LN 20
using namespace std;
int n,m,p[SZ+5];char s[SZ+5];
struct Data {int v,p;I Data(CI x=0,CI y=0):v(x),p(y){}
	I bool operator < (Con Data& o) Con {return v^o.v?v<o.v:p>o.p;}};
class SegmentTree
{
	private:
		#define PT CI l=1,CI r=m
		#define LT l,mid
		#define RT mid+1,r
		#define PU(x) (O[x].Mx=max(O[O[x].S[0]].Mx,O[O[x].S[1]].Mx))
		int Nt;struct node {Data Mx;int S[2];}O[SZ*40];
	public:
		I SegmentTree() {O[0].Mx=-1;}
		I void A(int& rt,CI x,PT)//单点加1
		{
			if(!rt&&(rt=++Nt),l==r) return (void)(++O[rt].Mx.v,O[rt].Mx.p=x);RI mid=l+r>>1;
			x<=mid?A(O[rt].S[0],x,LT):A(O[rt].S[1],x,RT),PU(rt);
		}
		I void Merge(int& x,RI y,PT)//线段树合并
		{
			if(!x||!y) return (void)(x|=y);if(O[++Nt]=O[x],x=Nt,l==r) return (void)(O[x].Mx.v+=O[y].Mx.v);
			RI mid=l+r>>1;Merge(O[x].S[0],O[y].S[0],LT),Merge(O[x].S[1],O[y].S[1],RT),PU(x);
		}
		I Data Q(CI rt,CI L,CI R,PT)//询问区间最大值及最早出现位置
		{
			if(L==l&&r==R) return O[rt].Mx;RI mid=l+r>>1;if(R<=mid) return Q(O[rt].S[0],L,R,LT);
			if(L>mid) return Q(O[rt].S[1],L,R,RT);return max(Q(O[rt].S[0],L,mid,LT),Q(O[rt].S[1],mid+1,R,RT));
		}
}S;
namespace SAM//后缀自动机
{
	int Nt=1,lst=1,c[SZ<<1],q[SZ<<1];struct node {int Rt,L,F[LN+1],S[30];}O[SZ<<1];
	I int A(CI x)//插入新字符
	{
		RI p=lst;if(O[p].S[x])//如果原先已有对应后继(直接把本函数最后两行代码贴过来删去o相关内容)
		{
			RI q=O[p].S[x];if(O[q].L==O[p].L+1) return lst=q;RI k=++Nt;//如果长度恰好大1
			(O[k]=O[q]).Rt=0,O[k].L=O[p].L+1,O[q].F[0]=k;W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F[0];return lst=k;//否则新建节点
		}
		RI o=lst=++Nt;O[o].L=O[p].L+1;W(p&&!O[p].S[x]) O[p].S[x]=o,p=O[p].F[0];if(!p) return O[o].F[0]=1,o;//新建节点,若该字符从未出现过
		RI q=O[p].S[x];if(O[q].L==O[p].L+1) return O[o].F[0]=q,o;RI k=++Nt;//如果长度恰好大1
		(O[k]=O[q]).Rt=0,O[k].L=O[p].L+1,O[q].F[0]=O[o].F[0]=k;W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F[0];return o;//否则新建节点
	}
	I void Work()//处理信息
	{
		RI i;for(i=1;i<=Nt;++i) ++c[O[i].L];for(i=1;i<=Nt;++i) c[i]+=c[i-1];for(i=1;i<=Nt;++i) q[c[O[i].L]--]=i;//按最大长度桶排
		RI j;for(i=2;i<=Nt;++i) for(j=1;j<=LN;++j) O[q[i]].F[j]=O[O[q[i]].F[j-1]].F[j-1];//预处理倍增数组
		for(i=Nt;i^1;--i) S.Merge(O[O[q[i]].F[0]].Rt,O[q[i]].Rt);//把当前点线段树合并到父节点
	}
	I int Jump(RI x,CI l) {for(RI i=LN;~i;--i) O[O[x].F[i]].L>=l&&(x=O[x].F[i]);return x;}//倍增找到最大长度大于等于l的最浅点
	I void Q(CI x,CI l,CI r) {Data t=S.Q(O[x].Rt,l,r);t.p?printf("%d %d
",t.p,t.v):printf("%d 0
",l);}//在对应点线段树中询问
}
int main()
{
	RI Qt,i,j,l,r,x,y;for(scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;++i) p[i]=SAM::A(s[i]&31);//记录每个前缀节点的编号
	for(scanf("%d",&m),i=1;i<=m;++i) for(scanf("%s",s+1),
		l=strlen(s+1),SAM::lst=j=1;j<=l;++j) S.A(SAM::O[SAM::A(s[j]&31)].Rt,i);//在每个前缀节点的线段树上给i加1
	SAM::Work(),scanf("%d",&Qt);W(Qt--) scanf("%d%d%d%d",&l,&r,&x,&y),SAM::Q(SAM::Jump(p[y],y-x+1),l,r);return 0;//倍增找到子串对应点
}
败得义无反顾,弱得一无是处
原文地址:https://www.cnblogs.com/chenxiaoran666/p/CF666E.html