P4770 [NOI2018] 你的名字 题解

P4770 [NOI2018] 你的名字

久闻大名。

遇到毒瘤应当先梳理题意:形式化地,给定一个模板串 \(S\) 和若干个询问串 \(T\),求 \(T\) 有多少个本质不同的子串满足其不是 \(S\) 中某一区间 \([l,r]\) 的子串。

发现 \(l=1,r=|S|\) 这种情况出题人给了 68 分,先试图考虑这种情况。题面中的这个 “不是” 正着做显然是不好做的,于是考虑用总的子串数减去本质不同的公共子串数。求公共子串到现在应当是基操,但是本质不同是我们需要考虑的问题。

考虑对 \(T\) 也建出 SAM,那么每加入一个节点后增加的答案实际上只有在母树上它的 \(\mathit{len}\) 减去它父亲的 \(\mathit{len}\) 值。于是每次在 \(T\) 的 SAM 上插入 \(T\) 的一个字符时记录其父亲的 \(\mathit{len}\) 值。再结合原本在 \(S\) 的 SAM 上匹配出的答案,相减,就能统计出每个点的贡献,用总的子串数减去即可。

代码
#include<bits/stdc++.h>
using namespace std;

constexpr int MAXN=1e6+5;
string s,t;
int Q;

struct{
	int tot=1,lst=1;
	struct SAM{
		int len,fa,s[26];
	}sam[MAXN];
	void ins(int c){
		sam[++tot].len=sam[lst].len+1;
		int pos=lst,ch=c-'a';
		lst=tot;
		while(pos&&!sam[pos].s[ch]){
			sam[pos].s[ch]=tot;
			pos=sam[pos].fa;
		}
		if(!pos) sam[tot].fa=1;
		else{
			int p=pos,q=sam[pos].s[ch];
			if(sam[p].len+1==sam[q].len) sam[tot].fa=q;
			else{
				sam[++tot]=sam[q];
				sam[tot].len=sam[p].len+1;
				sam[q].fa=sam[lst].fa=tot;
				while(pos&&sam[pos].s[ch]==q){
					sam[pos].s[ch]=tot;
					pos=sam[pos].fa;
				}
			} 
		}
	}
	void init(){
		memset(sam,0,sizeof(SAM)*(tot+1));
		tot=lst=1;
	}
}S,T;
long long fnd(){
	int pos=1;
	long long ans=0,res=0;
	for(int c:t){
		T.ins(c);
		c-='a';
		if(S.sam[pos].s[c]) res++,pos=S.sam[pos].s[c];
		else{
			while(pos&&!S.sam[pos].s[c]) pos=S.sam[pos].fa;
			if(!pos) res=0,pos=1;
			else res=S.sam[pos].len+1,pos=S.sam[pos].s[c];
		}
		ans+=T.sam[T.lst].len-T.sam[T.sam[T.lst].fa].len;
		if(res>=T.sam[T.sam[T.lst].fa].len) ans-=res-T.sam[T.sam[T.lst].fa].len;
	}
	return ans;
}

int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	S.init();
	cin>>s;
	for(auto x:s) S.ins(x);
	cin>>Q;
	while(Q--){
		int l,r;
		cin>>t>>l>>r;
		T.init();
		if(l==1&&r==(int)s.size()) cout<<fnd()<<'\n';
	}

	return 0;
}

然后考虑 \(l,r\) 任意。实际上这告诉我们,原本 SAM 上的有些边不能走了,能走的边只有所连接节点的 \(\text{endpos}\) 集合中有位于 \([l,r]\) 的点。

于是乎我们就需要用线段树合并来维护 SAM 的 \(\text{endpos}\) 集合(显然母树上父亲的 \(\text{endpos}\) 集合包含儿子的 \(\text{endpos}\) 集合,于是可以合并)。具体而言,就是用一个动态开点线段树,对于 SAM 上的每一个节点,如果该点的 \(\text{endpos}\) 集合中包含某一个位置就把这个位置加入以该节点为根的线段树中。然后合并,合并完就得到了每一个点的实际 \(\text{endpos}\) 集合。查询的时候需要查询一个区间代表的节点是否存在。

实际在代码实现中,我们维护的是区间最大值来使得不会误判。维护区间和也是可以的。

并且上文所说的查询区间,实际上是 \([l+\mathit{res},r]\),其中 \(\mathit{res}\) 是当前最长的匹配长度。

细节上需要注意的是,因为每一个节点都有可能被查询到,所以需要使用可持久化线段树合并,也就是每次合并需要新开一个节点。另外,每次失配不能直接在母树上跳父亲,因为判定条件和当前区间长度是相关的,需要等到查询的最长公共子串长度减小到其父亲的长度时再跳父亲。这部分的实现是较为特殊的。

#include<bits/stdc++.h>
using namespace std;

using ll=long long;
constexpr int MAXN=2e6+5;
string s,t;
int n,Q,l,r,rt[MAXN];

struct{
	#define lp st[p].lc
	#define rp st[p].rc
	int tot;
	struct SegTree{
		int lc,rc,c;
	}st[MAXN<<4];
	void pushup(int p){
		st[p].c=max(st[lp].c,st[rp].c);
	}
	void add(int x,int s,int t,int&p){
		if(!p) p=++tot;
		if(s==t) return st[p].c=x,void();
		int mid=(s+t)>>1;
		if(x<=mid) add(x,s,mid,lp);
		else add(x,mid+1,t,rp);
		pushup(p);
	}
	int mge(int p,int q,int l,int r){
		if(!p||!q) return p|q;
		int np=++tot;
		if(l==r) return st[np].c=max(st[p].c,st[q].c),np;
		int mid=(l+r)>>1;
		st[np].lc=mge(st[p].lc,st[q].lc,l,mid);
		st[np].rc=mge(st[p].rc,st[q].rc,mid+1,r);
		pushup(np);
		return np;
	}
	int ask(int l,int r,int s,int t,int p){
		if(!p||l>r||s>t||l>t||r<s) return 0;
		if(l<=s&&t<=r) return st[p].c;
		int mid=(s+t)>>1;
		return max(ask(l,r,s,mid,lp),ask(l,r,mid+1,t,rp));
	}
}B;

int head[MAXN],tt;
struct{
	int v,to;
}e[MAXN];
void addedge(int u,int v){
	e[++tt]={v,head[u]};
	head[u]=tt;
}
void dfs(int u){
	for(int i=head[u];i;i=e[i].to){
		dfs(e[i].v);
		rt[u]=B.mge(rt[u],rt[e[i].v],1,n);
	}
}

struct{
	int tot=1,lst=1;
	struct SAM{
		int len,fa,s[26];
	}sam[MAXN];
	void ins(int c,int fl){
		sam[++tot].len=sam[lst].len+1;
		int pos=lst,ch=c-'a';
		lst=tot;
		while(pos&&!sam[pos].s[ch]){
			sam[pos].s[ch]=tot;
			pos=sam[pos].fa;
		}
		if(!pos) sam[tot].fa=1;
		else{
			int p=pos,q=sam[pos].s[ch];
			if(sam[p].len+1==sam[q].len) sam[tot].fa=q;
			else{
				sam[++tot]=sam[q];
				sam[tot].len=sam[p].len+1;
				sam[q].fa=sam[lst].fa=tot;
				while(pos&&sam[pos].s[ch]==q){
					sam[pos].s[ch]=tot;
					pos=sam[pos].fa;
				}
			} 
		}
		if(fl) B.add(fl,1,n,rt[lst]);
	}
	void init(){
		memset(sam,0,sizeof(SAM)*(tot+1));
		tot=lst=1;
	}
}S,T;

ll fnd(){
	int pos=1,res=0;
	ll ans=0;
	for(int c:t){
		T.ins(c,0);
		c-='a';
        // 这里实现比较特别,我原来的那种实现总是 WA#22 97pts,用这个实现就能过
		while(1){
			if(S.sam[pos].s[c]&&B.ask(l+res,r,1,n,rt[S.sam[pos].s[c]])){
				pos=S.sam[pos].s[c];
				res++;
				break;
			}
			if(!res) break;
			res--;
			if(res==S.sam[S.sam[pos].fa].len) pos=S.sam[pos].fa;
		}
		ans+=T.sam[T.lst].len-T.sam[T.sam[T.lst].fa].len;
		if(res>=T.sam[T.sam[T.lst].fa].len) ans-=res-T.sam[T.sam[T.lst].fa].len;
	}
	return ans;
}

int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>s;
	n=s.size();
	s=' '+s;
	for(int i=1;i<=n;i++) S.ins(s[i],i);
	for(int i=2;i<=S.tot;i++) addedge(S.sam[i].fa,i);
	dfs(1);
	cin>>Q;
	while(Q--){
		cin>>t>>l>>r;
		T.init();
		cout<<fnd()<<'\n';
	}
	return 0;
}

又有思维难度又有代码难度,这就是后缀自动机。

posted @   Laoshan_PLUS  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示