G
N
I
D
A
O
L

【题解】P4770 [NOI2018] 你的名字

\(l=1,r=|S|\) 给了 \(68\) 分,先考虑这种情况。

本题询问的是 \(S\) 的子串,对它建立 sam 看看。

\(T\) 扔掉 \(S\) 的 sam 上匹配,用 SP1811 LCS - Longest Common Substring 类似的方式,设 \(lim_i\) 表示 \(T\) 的前 \(i\) 位走完以后,\(T\) 匹配的最长后缀,即 \(T[i-lim[i]+1:i]\)\(S\) 的子串。

\(lim_i\) 的求法如下:\(T\) 按照顺序每一位去匹配,如果当前点 \(u\) 存在这一位的转移边,那么 \(lim_i\larr lim_{i-1}+1\),否则不断跳 \(u\) 的fail树上祖先,直到存在一点 \(v\) 有该位的转移边,由于 \(v\) 内的点都是 \(u\) 的后缀,所以直接将 \(lim_i\larr len(v)+1\),如果不存在任何一点 \(v\) 有该位的转移边,那么 \(lim_i\larr 0\)

这样暴力跳fail的时间复杂度看似不对,我们来稍微分析一下。\(lim_i\) 在整个过程中不会增加超过 \(|T|\) 次,因此 \(lim_i\) 减少的次数不会超过 \(|T|\) 次,而每次跳 \(fail\) 时,\(len\) 会减小,于是对应可能的 \(lim_i\) 也会减小。而减小总次数不会超过 \(|T|\) 次,即跳fail操作不会进行超过 \(|T|\) 次,时间复杂度 \(O(|T|)\)

\(lim\) 求出来了以后,题目要我们求出若干本质不同的子串数量,于是放在 \(T\) 中去看,我们还需要构建 \(T\) 的 sam。 考虑 \(T\) 的 sam 上的每一个点带来的贡献,因为每个点 \(u\) 中代表的字符串是一些长度连续的 \(longest(u)\) 的后缀,这部分字符串可以拆成两组,一组是在 \(S\) 中完全出现过的,另一组是没有完全出现过的,且两组内部字符串长度都连续。考虑如何求出这个分界线,假设我们知道这些字符串在 \(T\) 中出现的任何一个结束位置 \(x\) ,我们就可以通过 \(lim_{x}\) 知道匹配长度。我们可以令 \(x\) 为最早出现位置方便转移。于是每个点 \(u\) 的贡献如下:

\[\max(0,len_u-\max(len_{fail_u},lim_x)) \]

原题 \(68\) 分 代码 (洛谷 \(54\) 分):

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=500009;
struct Node{
	ll ch[26],len,fail,tag;
};
#define ch(u,c) tr[u].ch[c]
#define fail(u) tr[u].fail
#define len(u) tr[u].len
#define tag(u) tr[u].tag
struct sam{
	ll lst,tot;
	Node tr[2*N];
	sam(){
		lst=tot=1;
	}
	inline void clear(){
		for(ll i=0;i<=tot;i++){
			len(i)=fail(i)=tag(i)=0;
			for(ll j=0;j<26;j++) ch(i,j)=0;
		}
		lst=tot=1;
	}
	inline void insert(ll c){
		ll p=lst,np=++tot;
		lst=np;
		len(np)=len(p)+1;
		tag(np)=len(np);
		for(;p&&!ch(p,c);p=fail(p)) ch(p,c)=np;
		if(!p) fail(np)=1;
		else{
			ll q=ch(p,c);
			if(len(q)==len(p)+1){
				fail(np)=q;
			}
			else{
				ll nq=++tot; tr[nq]=tr[q];
				len(nq)=len(p)+1;
				fail(np)=fail(q)=nq;
				for(;p&&ch(p,c)==q;p=fail(p)){
					ch(p,c)=nq;
				}
			}
		}
	}
} samS,samT;
ll lim[N];
inline void match(string &t){
	ll u=1;
	for(ll i=1;i<t.size();i++){
		ll c=t[i]-'a';
		if(samS.ch(u,c)){
			lim[i]=lim[i-1]+1;
			u=samS.ch(u,c);
		}
		else{
			for(;u&&!samS.ch(u,c);u=samS.fail(u));
			if(!u) lim[i]=0,u=1;
			else{
				lim[i]=samS.len(u)+1;
				u=samS.ch(u,c);
			}
		}
	}
}
string s,t;
ll nQ,l,r;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>s;
	s=" "+s;
	for(ll i=1;i<s.size();i++){
		samS.insert(s[i]-'a');
	}
	cin>>nQ;
	while(nQ--){
		cin>>t>>l>>r;
		t=" "+t;
		samT.clear();
		match(t);
		for(ll i=1;i<t.size();i++){
			samT.insert(t[i]-'a');
		}
		ll ans=0;
		for(ll u=2;u<=samT.tot;u++){
			ans+=max(0ll,samT.len(u)-max(samT.len(samT.fail(u)),lim[samT.tag(u)]));
		}
		cout<<ans<<"\n";
	}
	return 0;
}

再考虑 \(l,r\) 任意的情况。一般来说处理区间问题需要维护每个点的 \(endpos\) 集合,使用线段树合并即可。看一下 \(lim\) 产生的变化。我们在求 \(lim\) 的时候,需要看一下转移后的点是否存在 \(endpos\in[l+lim,r]\) ,如果存在那么不影响,如果不存在则我们需要不断跳fail知道满足条件,注意这里 \(len\) 也要改为,查找该点 \(endpos\) 中在 \([1,r]\) 的最大值 \(k\),然后用原先的 \(len\)\(k-l+1\)\(\min\) 才是需要的 \(len\)。也可以无脑直接让 \(lim\) 不断缩小,直到变为 \(fail\)\(len\) 再转移。

时间复杂度 \(O((|S|+|T|)\log |S|)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=500009;
struct Node{
	ll ch[26],len,fail,tag,vst;
};
#define ch(u,c) tr[u].ch[c]
#define fail(u) tr[u].fail
#define len(u) tr[u].len
#define tag(u) tr[u].tag
#define vst(u) tr[u].vst
struct Segment{
	ll ls,rs;
} tr[N*40];
ll cnt;
ll rt[N*2];
#define ls(u) tr[u].ls
#define rs(u) tr[u].rs
inline void upd(ll &u,ll l,ll r,ll x){
	if(!u) u=++cnt;
	if(l==r) return;
	ll mid=(l+r)/2;
	if(x<=mid) upd(ls(u),l,mid,x);
	else upd(rs(u),mid+1,r,x);
}
inline ll merge(ll x,ll y){
	if(!x||!y) return x+y;
	ll u=++cnt;
	ls(u)=merge(ls(x),ls(y));
	rs(u)=merge(rs(x),rs(y));
	return u;
}
inline bool query(ll u,ll l,ll r,ll ql,ll qr){
	if(!u||ql>qr) return 0;
	if(ql<=l&&r<=qr) return 1;
	ll mid=(l+r)/2;
	if(ql<=mid){
		if(query(ls(u),l,mid,ql,qr)) return 1;
	}
	if(mid<qr){
		if(query(rs(u),mid+1,r,ql,qr)) return 1;
	}
	return 0;
}
string s;
struct sam{
	ll lst,tot;
	Node tr[2*N];
	sam(){
		lst=tot=1;
	}
	inline void clear(){
		for(ll i=0;i<=tot;i++){
			len(i)=fail(i)=tag(i)=vst(i)=0;
			for(ll j=0;j<26;j++) ch(i,j)=0;
		}
		lst=tot=1;
	}
	inline void insert(ll c){
		ll p=lst,np=++tot;
		lst=np;
		len(np)=len(p)+1;
		tag(np)=len(np),vst(np)=1;
		for(;p&&!ch(p,c);p=fail(p)) ch(p,c)=np;
		if(!p) fail(np)=1;
		else{
			ll q=ch(p,c);
			if(len(q)==len(p)+1){
				fail(np)=q;
			}
			else{
				ll nq=++tot; tr[nq]=tr[q];
				len(nq)=len(p)+1;
				vst(nq)=0;
				fail(np)=fail(q)=nq;
				for(;p&&ch(p,c)==q;p=fail(p)){
					ch(p,c)=nq;
				}
			}
		}
	}
	ll bin[2*N],xs[2*N];
	inline void build(){
		for(ll i=1;i<=tot;i++) bin[len(i)]++;
		for(ll i=1;i<=tot;i++) bin[i]+=bin[i-1];
		for(ll i=1;i<=tot;i++) xs[bin[len(i)]--]=i;
		for(ll i=tot;i>=1;i--){
			ll p=xs[i];
			if(vst(p)) upd(rt[p],1,s.size()-1,len(p));
			rt[fail(p)]=merge(rt[fail(p)],rt[p]);
		}
	}
} samS,samT;
ll lim[N];
inline void match(string &t,ll l,ll r){
	ll u=1,len=0;
	for(ll i=1;i<t.size();i++){
		ll c=t[i]-'a';
		while(1){
			if(samS.ch(u,c)&&query(rt[samS.ch(u,c)],1,s.size()-1,l+len,r)){
				len++;
				u=samS.ch(u,c);
				break;
			}
			if(u==1) break;
			len--;
			if(len==samS.len(samS.fail(u))){
				u=samS.fail(u);
			}
		}
		lim[i]=len;
	}
}
string t;
ll nQ,l,r;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>s;
	s=" "+s;
	for(ll i=1;i<s.size();i++){
		samS.insert(s[i]-'a');
	}
	samS.build();
	cin>>nQ;
	while(nQ--){
		cin>>t>>l>>r;
		t=" "+t;
		samT.clear();
		match(t,l,r);
		for(ll i=1;i<t.size();i++){
			samT.insert(t[i]-'a');
		}
		ll ans=0;
		for(ll u=2;u<=samT.tot;u++){
			ans+=max(0ll,samT.len(u)-max(samT.len(samT.fail(u)),lim[samT.tag(u)]));
		}
		cout<<ans<<"\n";
	}
	return 0;
}
posted @ 2025-06-29 11:50  QWQcoding  阅读(24)  评论(0)    收藏  举报