6567. 【GDOI2020模拟】字符串

题目

给你一个字符串,问所有长度为 m m m的字符串之中,对于子串 i i i,和它相似的子串分别是什么。
“相似”的概念:两个字符串至多有一个位置的字符不同。
n ≤ 1 e 5 n\leq 1e5 n1e5


正解

由于比赛的时候基本上都在刚T1,所以这题没有干过。

各种暴力,大概都是从快速地判断子串相等入手。
但是正解用到了一个新的性质:对于字符串 S S S T T T,若 l c p ( S , T ) + l c s ( S , T ) ≥ m − 1 lcp(S,T)+lcs(S,T)\geq m-1 lcp(S,T)+lcs(S,T)m1,则他俩相似。
原因不解释。

顺着这个思路,很容易(可能)想到建后缀树和前缀树。
建树直接跑SAM,SAM的 f a i l fail fail树就是反串的后缀树。
对于两个字符串 u u u v v v(这个是字符串的编号,并不是字符串的端点):
它们在后缀树上的 L C A LCA LCA的深度(SAM中的 l e n len len),便是它们 l c p lcp lcp
那么我们考虑在后缀树的 L C A LCA LCA处计算贡献:以某个点为根的时候, l c p lcp lcp定下来了。枚举属于两个不同子树中的点,通过前缀树计算他们的 l c s lcs lcs,如果 l c s ≥ m − 1 − l c p lcs\geq m-1-lcp lcsm1lcp,它们就是合法的一对。
思考 l c p lcp lcp确定的时候,对于某个点 u u u,合法的 v v v满足什么。 u u u v v v在前缀树上的 L C A LCA LCA的深度大于某个值,所以合法的 v v v在某棵子树内。

接下来才是具体做法:
考虑dsu on tree(可以上网搜,本质上和启发式合并差不多),先处理完轻儿子,将轻儿子的信息清空;处理重儿子,然后将重儿子的信息继承过来,暴力每个轻儿子,维护信息。
考虑 A A A B B B两个树连通块合并,前者比后者大,按照启发式合并的思想,暴力枚举 B B B内的点。
用个数据结构来维护一下在前缀树中的dfs序区间内,对应的后缀树上的点出现在 A A A的点有多少个。支持单点加,区间查就好了。
对于点 u u u,倍增算出合法的 v v v在哪个节点的子树内,然后在数据结构上查。
整个 B B B处理完之后,就合并入 A A A中,具体来说就是将他们每个点往数据结构里加。
最后,在清空信息的时候,不要忘了数据结构上的信息也要一同清空。

然而,我们只是做到了 ( u , v ) (u,v) (u,v)的贡献挂在 u u u上( u ∈ B u \in B uB),并没有挂在 v v v上。
于是我们再维护一个数据结构。这个支持区间加,单点查:对于点 u u u,求出合法的 v v v在哪个节点的子树内,然后在数据结构上对应的区间中加。在最后输出答案的时候单点查加上贡献。
不过要注意一下:当 u u u从集合 B B B中进入集合 A A A中,它身上本来是不带贡献的(指 u ∈ A u\in A uA时与某些 B B B产生的贡献),但是它在某次修改中被连带着“误改”过。这怎么办呢?
如果用线段树,可以用粗暴下传标记的方式来解决这个问题。
其实没有这个必要。既然是“误改”的,那就在答案中剪掉嘛……(另外记得,清空信息的时候,单点查询,将贡献计入答案)
所以,只需要用树状数组就可以解决这个问题。
总时间复杂度 O ( n lg ⁡ 2 n ) O(n\lg^2 n) O(nlg2n)

另外,C_C讲题的时候讲了个在SA上分治的做法。具体就是在区间中找到 h e i g h t height height最小的位置,将区间分成两半。这个方法和我上面将的这个方法本质上并没有多大区别,因为众所周知,后缀数组就是后缀树的dfs序, h e i g h t height height就是相邻两个点的 L C A LCA LCA深度。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100010
#define ll long long
int n,m;
char str[N];
struct Node{
	Node *c[26],*fail;
	int len;
} d[N*4],*S1,*S2,*T;
int cnt;
inline void insert(char ch,Node *S){
	Node *nw=&d[++cnt];
	nw->len=T->len+1;
	Node *p=T;
	for (;p && !p->c[ch-'a'];p=p->fail)
		p->c[ch-'a']=nw;
	if (!p)
		nw->fail=S;
	else{
		Node *q=p->c[ch-'a'];
		if (q->len==p->len+1)
			nw->fail=q;
		else{
			Node *clone=&d[++cnt];
			memcpy(clone,q,sizeof *q);
			clone->len=p->len+1;
			q->fail=nw->fail=clone;
			for (;p && p->c[ch-'a']==q;p=p->fail)
				p->c[ch-'a']=clone;
		}
	}
	T=nw;
}
int id1[N],id2[N],re[N*4];
struct EDGE{
	int to;
	EDGE *las;
} e[N*4];
int ne;
EDGE *last[N*4];
int rt1,rt2;
int fa[N*4][20];
int in[N*4],out[N*4],tot;
int dep[N*4],siz[N*4],hs[N*4];
void init(int x){
	for (int i=1;1<<i<=dep[x];++i)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	in[x]=++tot;
	for (EDGE *ei=last[x];ei;ei=ei->las)
		init(ei->to);
	out[x]=tot;
}
void getsiz(int x){
	siz[x]=1;
	for (EDGE *ei=last[x];ei;ei=ei->las){
		getsiz(ei->to);
		siz[x]+=siz[ei->to];
		if (siz[ei->to]>siz[hs[x]])
			hs[x]=ei->to;
	}
}
int find(int x,int tar){
	if (tar>dep[x])
		return 0;
	tar=max(tar,0);
	for (int i=19;i>=0;--i)
		if (dep[fa[x][i]]>=tar)
			x=fa[x][i];
	return x;
}
int _t[N*2],s[N*2];
inline void add(int x,int c,int *t=_t){
	for (;x<=tot;x+=x&-x)
		t[x]+=c;
}
inline int query(int x,int *t=_t){
	int res=0;
	for (;x;x-=x&-x)
		res+=t[x];
	return res;
}
ll sum,ans[N];
void scan(int x,int lim){
	if (re[x]!=-1){
		int y=find(id2[re[x]+m-1],lim);
		if (y){
			ans[re[x]]+=query(out[y])-query(in[y]-1);
			add(in[y],1,s),add(out[y]+1,-1,s);
		}
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		scan(ei->to,lim);
}
void insert(int x,int c){
	if (re[x]!=-1){
		add(in[id2[re[x]+m-1]],c);
		ans[re[x]]-=c*query(in[id2[re[x]+m-1]],s);
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		insert(ei->to,c);
}
void dfs(int x){
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=hs[x]){
			dfs(ei->to);
			insert(ei->to,-1);
		}
	if (hs[x])
		dfs(hs[x]);
	if (re[x]!=-1){
		int y=find(id2[re[x]+m-1],m-1-dep[x]);
		if (y){
			ans[re[x]]+=query(out[y])-query(in[y]-1);
			add(in[y],1,s),add(out[y]+1,-1,s);
		}
		add(in[id2[re[x]+m-1]],1);
		ans[re[x]]-=query(in[id2[re[x]+m-1]],s);
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=hs[x]){
			scan(ei->to,m-1-dep[x]);
			insert(ei->to,1);
		}
}
int main(){
	freopen("string.in","r",stdin);
	freopen("string.out","w",stdout);
	scanf("%d%d%s",&n,&m,str);
	T=S1=&d[++cnt];
	memset(re,255,sizeof re);
	for (int i=n-1;i>=0;--i){
		insert(str[i],S1),id1[i]=T-d;
		re[T-d]=(i+m-1<n?i:-1);
	}
	T=S2=&d[++cnt];
	for (int i=0;i<n;++i)
		insert(str[i],S2),id2[i]=T-d;
	dep[0]=-1;
	for (int i=1;i<=cnt;++i){
		dep[i]=d[i].len;
		if (d[i].fail==NULL)
			continue;
		fa[i][0]=d[i].fail-d;
		e[ne]={i,last[fa[i][0]]};
		last[fa[i][0]]=e+ne++;
	}
	rt1=S1-d,rt2=S2-d;
	init(rt2);
	getsiz(rt1);
	dfs(rt1);
	for (int i=0;i+m-1<n;++i)
		ans[i]+=query(in[id2[i+m-1]],s);
	for (int i=0;i+m-1<n;++i)
		printf("%d ",ans[i]);
	return 0;
}


总结

其实这题并不是很难吧,但是细节有点多,害我搞了一天(应该跟早上边打程序边学习政治有关系,所以得到教训:调试的时候可以分心,打程序的时候尽量不要分心,不然细节很容易挂)
然后就是dsu on tree这个东西,说实话本质上是个用脚趾头都能想到的启发式合并。不过也应该要掌握这个概念,不要光想着尽管和它差不多的启发式合并。
最后:我发现我现在真是越来越不能打SA了。SA虽然思想简单,但细节遍地都是,极其难写。半年还是一年来见到SA我都会用SAM代替。然而,
SA是我必须要度过的难关,因为——SAM的空间不是那么好承受啊(

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值