bzoj5417 [NOI2018] 你的名字

传送门

Part 1:l=1,r=|S|

先考虑部分数据:询问中满足 l = 1 , r = ∣ S ∣ l=1,r=|S| l=1,r=S
考虑 T T T串的一个前缀:对于 T [ 1... i ] T[1...i] T[1...i],如果满足 T [ p . . . i ] ( 1 ≤ p ≤ i ) T[p...i](1≤p≤i) T[p...i](1pi) S S S的子串,且 T [ p − 1... i ] T[p-1...i] T[p1...i]不是 S S S的子串(我们令 T [ p − 1... i ] T[p-1...i] T[p1...i] T [ 1... i ] T[1...i] T[1...i]对于 S S S的最长匹配后缀,令它的长度为 l e n [ i ] len[i] len[i],显然 p = i − l e n [ i ] + 1 p=i-len[i]+1 p=ilen[i]+1。然而当一个也匹配不上时, l e n [ i ] = 0 len[i]=0 len[i]=0),那么显然 T [ 1... i ] , T [ 2... i ] , T [ 3... i ] . . . . . . , T [ p − 1... i ] T[1...i],T[2...i],T[3...i]......,T[p-1...i] T[1...i],T[2...i],T[3...i]......,T[p1...i]都是满足条件的(即是 T T T的一个非空连续子串且一定没有在 S S S中出现)
那么我们只要考虑对于每一个 i i i,求出它的 l e n len len值, i i i位置的贡献就是 i − l e n i-len ilen。然而这样直接加起来是不对的。因为这里只统计本质不同的串
为了解决这个问题,我们建立出 T T T串的后缀自动机。因为后缀自动机上所有节点包含的所有串即为 T T T串的所有本质不同的子串,可以达到去重的效果。而一个节点包含的所有串一定形如 c d e , b c d e , a b c d e cde,bcde,abcde cde,bcde,abcde,长度连续且一个串是比它长的串的后缀,并且这些串的所有的出现位置是一样的。假设我们现在已经求出了 l e n len len数组。我们考虑一个节点的贡献。
我们假设这个节点里最短的串长为 m i n L minL minL,最长的串长为 m a x L maxL maxL,令它们在 T T T中出现的 e n d p o s endpos endpos集合中的一个元素为 p o s pos pos(随便选一个位置即可,因为这些位置是等价的)。于是这个节点中的贡献(我们只考虑当前节点中的这些串是否在 S S S中出现!)就是 a n s = m a x ( 0 , m a x L − m a x ( m i n L − 1 , l e n [ p o s ] ) ) ans=max(0,maxL-max(minL-1,len[pos])) ans=max(0,maxLmax(minL1,len[pos]))
如下图为后缀自动机中的一个节点包含的串。绿色表示符合条件,红色表示不符。


(如果 l e n [ p o s ] &gt; = m a x L len[pos]&gt;=maxL len[pos]>=maxL,那么该节点中所有点都不符合要求,因为它们肯定都在 S S S中出现过了,想想 l e n len len的含义。如果 l e n [ p ] &lt; m i n L len[p]&lt;minL len[p]<minL,说明它们肯定都没有在 S S S中出现过,那么这个节点中的所有串都符合要求)
那么我们最后在后缀自动机上跑一遍统计每个节点的答案就行了。
于是当前的问题就只剩下如何求出 l e n len len数组了——
考虑增量法:假设我们已经求出了 l e n [ i − 1 ] len[i-1] len[i1]。现在要往 T T T后面添加一个字符,并求出 l e n [ i ] len[i] len[i]。它只用在 l e n [ i − 1 ] len[i-1] len[i1]的基础上修改就行了。
我们记 l e n [ i ] = L len[i]=L len[i]=L。那么 L L L初始值为 l e n [ i − 1 ] len[i-1] len[i1]在求解过程中, L L L是在不断调整的。
第一种情况:就是 T [ ( i − 1 ) − L + 1... i ] T[(i-1)-L+1...i] T[(i1)L+1...i] S S S中出现过,那么 L = L + 1 L=L+1 L=L+1。因为 T [ ( i − 1 ) − L + 1... i ] T[(i-1)-L+1...i] T[(i1)L+1...i]这个串不可能再往左边扩展了。(回去看 l e n [ i ] len[i] len[i]定义)
在这里插入图片描述
第二种情况:我们发现 T [ ( i − 1 ) − L + 1... i ] T[(i-1)-L+1...i] T[(i1)L+1...i]没有在 S S S中出现过,那么我们把最左边那个字符抠掉,并且把当前的 L L L减去 1 1 1。看现在这个串是否出现过。如果一直没有出现过直到 l e n len len值为 0 0 0,这个时候就可以退出,去求 l e n [ i + 1 ] len[i+1] len[i+1]了。因为一个也匹配不上。
以上可以看做是 T T T串的两个指针在跳动。

这个过程可以通过跳 S S S的后缀自动机实现。后缀自动机上,从根节点到任意一个节点的路径上的字符连起来就代表着 S S S的一类子串(字符相同但位置不同)。当我们在上面跳,就相当于用 当前串 和 S S S的所有本质不同子串 去匹配。
上次的 T [ ( i − 1 ) − L + 1... i − 1 ] T[(i-1)-L+1...i-1] T[(i1)L+1...i1]已经和 S S S的某个子串匹配上了,我们令这个时候停在 u u u节点上,这就意味着从根节点到 u u u的某条路径可以构成 T [ ( i − 1 ) − L + 1... ( i − 1 ) ] T[(i-1)-L+1...(i-1)] T[(i1)L+1...(i1)]。然后我们有当前字符 T [ i ] = c T[i]=c T[i]=c,现在要做的事情就是 T [ i − L + 1... i ] T[i-L+1...i] T[iL+1...i]是否在 S S S中出现过。如果 u u u通过边 c c c连向 v v v,说明它出现了。我们可以直接跳到 v v v上,并且把 L L L加一,这时 T [ i − L + 1... i ] T[i-L+1...i] T[iL+1...i] S S S的子串。而如果不匹配,我们就把 L L L减去一。直到 T [ i − L + 1... i ] T[i-L+1...i] T[iL+1...i]成为 S S S的子串或 L = 0 L=0 L=0
在上述过程中,如果当前串 T [ ( i − 1 ) − L + 1... i ] T[(i-1)-L+1...i] T[(i1)L+1...i]已经不在节点 u u u的包含范围内了,我们就把 u u u跳到它 p a r e n t parent parent树的父节点上去。因为它的 e n d p o s endpos endpos集合变大了。匹配的机会也变多了。

那么最后得出的 L L L就是 l e n [ i ] len[i] len[i]。综上, l = 1 , r = ∣ S ∣ l=1,r=|S| l=1,r=S的情况便得以解决。

Part 2:l,r任意

这时我们只需要多关心一个问题: T T T的某个子串如果在 S S S中出现,它出现在哪里?
令该子串的长度为 l e n g t h length length,那么如果它在 S S S中的 e n d p o s endpos endpos集合至少有一个元素 p o s pos pos满足 p o s ∈ [ l + l e n g t h , r ] pos∈[l+length,r] pos[l+length,r],就意味着它在询问的 S [ l , r ] S[l,r] S[l,r]范围中出现过。
我们所要做修改的步骤是 T T T S S S的匹配过程,也就是求 l e n len len的过程。
这里唯一不同的一点便是——现在如果在 u u u节点上,有边 c c c连向 v v v,我们不能直接跳过去。
因为如果 T [ i − L + 1... i ] T[i-L+1...i] T[iL+1...i] e n d p o s endpos endpos集合中没有任何元素在 [ l + L , r ] [l+L,r] [l+L,r]范围内,它也不和 S S S匹配。这便是我们要判断的。

这个时候我们用线段树维护 S S S的后缀自动机上节点的 e n d p o s endpos endpos集合。
线段树中节点的 l , r l,r l,r表示的是在字符串中的位置。
当我们建立 S S S的后缀自动机,每插入一个节点时,我们在这个节点记下插入的位置。这相当于一个标记——从当前节点到根节点的 e n d p o s endpos endpos集合中都包含这个位置。建完之后再在自动机的 p a r e n t parent parent树上从下往上把线段树合并,即把父节点变为它跟儿子合并之后的节点。这样就维护了每个节点的 e n d p o s endpos endpos集合。

这里的线段树用了动态开点。也就是说,有这个节点表示:这个节点所代表的范围内有 e n d p o s endpos endpos。询问的时候只用看这个节点是否存在即可。

代码中的 t a g tag tag只是随意找的一个位置。之前说过了。

#include<bits/stdc++.h>
#define ll long long
#define mid ((l+r)>>1)
using namespace std;
const int maxn=5e5+10;
char S[maxn],T[maxn<<1];int ss,tt;
int Q,l,r,L,R,cnt,tot=0,p[maxn<<1],lim[maxn<<1];
namespace SGT{
	struct Node{int l,r;}t[maxn<<6];
	int root[maxn<<1];
	inline int merge(int x,int y){
		if(!x||!y) return x+y;
		int now=++tot;
		t[now].l=merge(t[x].l,t[y].l);
		t[now].r=merge(t[x].r,t[y].r);
		return now;
	}
	inline void insert(int &rt,int l,int r,int pos){
		if(!rt) rt=++tot;
		if(l==r) return;
		if(pos<=mid) insert(t[rt].l,l,mid,pos);
		else insert(t[rt].r,mid+1,r,pos);
	}
	inline int query(int rt,int l,int r,int x,int y){
		if(!rt||x>y) return 0;
		if(x<=l&&r<=y) return 1;
		if(x<=mid&&query(t[rt].l,l,mid,x,y)) return 1;
		if(y>mid&&query(t[rt].r,mid+1,r,x,y)) return 1;
		return 0;
	}
}
using namespace SGT;
namespace SAM{
	struct node{
		int link,len,tag,nxt[26];
		inline void clear(){tag=link=len=0,memset(nxt,0,sizeof(nxt));}
	};
	struct Sam1{
		node st[maxn<<1];int last,sz;
		int sum[maxn],p[maxn<<1];
		inline void init(){
			for(int i=0;i<=sz;++i) st[i].clear();
			last=sz=0,st[0].link=-1;
		}
		inline void build(int c){
			int cur=++sz,p=last;
			st[cur].len=st[last].len+1;
			for(;p!=-1&&!st[p].nxt[c];p=st[p].link)
				st[p].nxt[c]=cur;
			if(p==-1) st[cur].link=0;
			else{
				int q=st[p].nxt[c];
				if(st[p].len+1==st[q].len) st[cur].link=q;
				else{
					int clone=++sz;st[clone]=st[q];
					st[clone].len=st[p].len+1;
					for(;p!=-1&&st[p].nxt[c]==q;p=st[p].link)
						st[p].nxt[c]=clone;
					st[cur].link=st[q].link=clone;
				}
			}last=cur;
		}
		inline void pre_work(){
			for(int i=1;i<=sz;++i) sum[st[i].len]++;
			for(int i=1;i<=ss;++i) sum[i]+=sum[i-1];
			for(int i=1;i<=sz;++i) p[sum[st[i].len]--]=i;
			for(int i=sz;i>=1;--i) root[st[p[i]].link]=merge(root[st[p[i]].link],root[p[i]]);
		}
	}Sam_S;
	struct Sam2{
		node st[maxn<<1];int last,sz;
		inline void init(){
			for(int i=0;i<=sz;++i) st[i].clear();
			last=sz=0,st[0].link=-1;
		}
		inline void build(int c,int id){
			int cur=++sz,p=last;
			st[cur].len=st[p].len+1,st[cur].tag=id;
			for(;p!=-1&&!st[p].nxt[c];p=st[p].link)
				st[p].nxt[c]=cur;
			if(p==-1) st[cur].link=0;
			else{
				int q=st[p].nxt[c];
				if(st[p].len+1==st[q].len) st[cur].link=q;
				else{
					int clone=++sz;st[clone]=st[q];
					st[clone].len=st[p].len+1;
					for(;p!=-1&&st[p].nxt[c]==q;p=st[p].link)
						st[p].nxt[c]=clone;
					st[cur].link=st[q].link=clone;
				}
			}last=cur;
		}
		inline ll calc(ll ret=0){
			for(int i=1;i<=sz;++i) ret+=max(0,st[i].len-max(st[st[i].link].len,lim[st[i].tag]));
			return ret;
		}
	}Sam_T;
}
using namespace SAM;
int main(){
	//freopen("3675.in","r",stdin);
	scanf("%s%d",S+1,&Q),ss=strlen(S+1),Sam_S.init();
	for(int i=1;i<=ss;++i)
		Sam_S.build(S[i]-'a'),insert(root[Sam_S.last],1,ss,i);
	Sam_S.pre_work();
	while(Q--){
		scanf("%s%d%d",T+1,&L,&R);
		tt=strlen(T+1),Sam_T.init();
		for(int i=1,u=0,len=0,c,v;i<=tt;++i){
			c=T[i]-'a',Sam_T.build(c,i);
			while(1){
				v=Sam_S.st[u].nxt[c];
				if(v&&query(root[v],1,ss,L+len,R)){u=v,++len;break;}
				if(!len) break;
				if((--len)==Sam_S.st[Sam_S.st[u].link].len) u=Sam_S.st[u].link;
			}lim[i]=len;
		}printf("%lld\n",Sam_T.calc());
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值