CF316G3 - Good Substrings

发布时间 2023-05-16 22:01:15作者: jucason_xu

有点厉害。

首先给一个 \(\text{SAM}\) 的做法。我们先对所有串建立 \(\text{SAM}\),然后对于每个 \(T\),我们先预处理对于每个节点对应的 \(endpos\) 集合,多长的子串可以满足条件。

然后,我们把 \(S\) 串往当前的自动机输入,每次会来到一个 \(endpos\) 集合。同时再输入的过程中保留 \(curlen\) 表示当前匹配上的长度,如果匹配上了就 \(+1\),如果失配了就和当前失配节点的 \(len\)\(\min\),直到匹配上为止。然后我们就得到了 \(S\) 中所有在 \(i\) 位置结束的子串,在当前限制下的可用长度区间。(注意还要和 \(curlen\)\(\min\),因为那前面的根本在这个限制串匹配不上)

最后将所有的限制区间取交集,得到每个位置可用的答案区间。这还没完,因为我们需要的是原串本质不同的子串,所以需要再建一个 \(\text{SAM}\),对于同一个 \(endpos\) 集合,长度落在 \(len\) 内的所有贡献,每个 \(endpos\) 集合只计算一次。然后就可以通过了,复杂度 \(O(|S|+n|T|)\)

struct node{
	int len,sz,pa,ss,nxt[26];
	node(){
		len=0,sz=0,pa=0;
		rd(i,26)nxt[i]=0;
	} 
}s[100005];
vt<int>vv[100005];
int lst,cnt,ss[100005];
inline void add(int c,int v){
	int x=lst;s[++cnt]=node();
	s[cnt].len=s[lst].len+1;
	s[lst=cnt].sz=1;ss[cnt]=v;
	for(;x&&!s[x].nxt[c];x=s[x].pa)s[x].nxt[c]=cnt;
	if(!x)return (void)(s[cnt].pa=1);
	
	int to=s[x].nxt[c];
	if(s[x].len+1==s[to].len){
		return(void)(s[cnt].pa=to);
	}
	
	s[++cnt]=s[to];
	s[cnt].sz=0;
	s[cnt].len=s[x].len+1;
	for(;x&&s[x].nxt[c]==to;x=s[x].pa)s[x].nxt[c]=cnt;
	s[cnt-1].pa=s[to].pa=cnt;
}
int l,r,L[100005],R[100005];
inline void dfs(int x){
	for(auto j:vv[x]){
		dfs(j);
		s[x].sz+=s[j].sz;
		ss[x]=ss[j];
	}
	if(l<=s[x].sz&&s[x].sz<=r){
		L[x]=s[s[x].pa].len+1;
		R[x]=s[x].len;
	}else L[x]=114514,R[x]=0;
}
inline void getcur(int x){
	if(x!=1)L[x]=min(L[x],L[s[x].pa]),R[x]=max(R[x],R[s[x].pa]);
	for(auto j:vv[x])getcur(j);
}
inline void build(string t){
	cnt=1,lst=1,s[1]=node();
	rd(i,(int)t.size())add(t[i]-'a',i);
	rp(i,cnt)vv[i].clear();
	rep(i,2,cnt)vv[s[i].pa].pb(i);
	dfs(1);s[1].sz=0;getcur(1);
}
int ans=0;
int ansl[50005],ansr[50005];
inline void solve(int x){
	l=s[s[x].pa].len+1,r=R[x]=s[x].len;
	l=max(l,ansl[ss[x]]);
	r=min(r,ansr[ss[x]]);
	ans+=max(0,r-l+1);
	for(auto j:vv[x])solve(j);
}
st T,S;
int n,m,cx=0;
inline int trans(int x,int c){
	while(x&&!s[x].nxt[c])x=s[x].pa,cx=min(cx,s[x].len);
	if(x){
		cx++;
		return s[x].nxt[c];
	}
	return 1;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>T>>n;
	m=T.size();
	rd(i,m)ansl[i]=1,ansr[i]=i+1;
	rp(_,n){
		cin>>S>>l>>r;
		if(r>0){
			build(S);
			cx=0;
			for(int x=1,i=0;i<m;i++){
				x=trans(x,T[i]-'a');
				if(l==0&&L[x]==114514);
				else ansl[i]=max(ansl[i],L[x]);
				if(l!=0)ansr[i]=min({ansr[i],R[x],cx});
			}
		}else{
			build(S);
			cx=0;
			for(int x=1,i=0;i<m;i++){
				x=trans(x,T[i]-'a');
				ansl[i]=max(ansl[i],s[x].len+1);
			}
		}
	}
	build(T);
	solve(1);
	cout<<ans<<endl;
	return 0;
}
//Crayan_r

然后是 \(\text{SA}\) 做法。首先把所有的 \(S\)\(T\) 隔着特殊字符连接起来,然后求后缀数组。然后是用高度数组实现的求两个后缀的 \(lcp\)。这都是基本操作。

然后,我们枚举 \(S\) 的后缀,对每个后缀和每条限制,一定存在一个区间 \([L,R]\) 使得这个后缀长度为 \([L,R]\) 的前缀在这个区间内的出现次数满足条件。

我们可以预处理每个 \(T\)\(rnk\) 小于等于某个数的后缀个数。这一步有的选手使用了平衡树维护,实际上是不需要的。我们可以直接记录前缀和,直接得到当前长度的前缀在 \(T\) 中的出现次数。

\(O(1)\) 判断当前长度是否满足要求,我们就可以两次二分分别求出 \([L,R]\),注意它们两个是独立的,不互相影响。

然后还是合并各个限制答案,我们就得到了对于每个后缀,长度是多少的子串满足要求。

最后就是统计本质不同子串的答案。我们可以在原串上按照排名依次来,当前串 \(i\) 和下一个串 \(i+1\),所有长度在 \(lcp\) 以内的答案都已经被 \(i\) 统计过了,所以 \(i+1\) 只需要统计 \(lcp\) 以外的部分。这样就不会重复统计子串。

然后就可以通过了。

两种做法的内在联系

实际上我们就是用 \(\text{SA}\)\(\text{SAM}\) 通过相同的中间步骤做着相同的事情。因为 \(\text{SA}\)\(\text{SAM}\) 其实是可以互相替代的。

我们第一个程序用 \(\text{SAM}\) 求出了每个前缀多长的后缀满足要求。第二个程序用 \(\text{LCP}\) 求出了每个后缀多长的前缀满足要求。求法实际上也是相同的。二分 \(lcp\) 长度其实就是在失配树上倍增跳父亲,只不过在 \(\text{SAM}\) 中存在失配树从上往下的关系,并且询问时静态的,所以我们直接从上往下递推了。两者的答案其实是一样的。

后面统计答案的过程就更加相似了。其实一直都是基于查找相同 \(lcp\) 的后缀排名区间来替代 \(endpos\) 集合的作用。两者其实是非常相似的。