【题解】[NOI2018]你的名字
首先考虑 怎么做。
那么问题就变成了 中有多少本质不同子串没有在 中出现过。
考虑做一个差分,就变成了:有多少本质不同子串在 中出现过。
考虑对每个 的前缀 求 在 中出现的最长后缀长度。
发现这个其实就是一个在与 求匹配的过程,同时也求出了它们的最长公共子串。
考虑直接把 扔到 的后缀自动机上面跑,每一次更新就看看能不能走转移边,能走就直接让匹配长度 否则跳后缀链接,跳到能走为止。
这样就可以线性处理出对于 的每一个前缀对应出现的后缀最长长度了。同时,对于每个这样的长度,小于等于它的必然是在 中出现过的。
之所以要求后缀其实对应了一个性质:子串就是所有前缀的后缀。所以这一系列匹配的过程就对于每一个前缀都求了出来最长出现的后缀长度。
而又因为其 endpos 等价类中它是连续的,所以统计答案的时候就需要用对应节点的所有后缀个数减去其中 endpos 对应的最长后缀匹配长度。因为这些是连续的,直接减去就好。
注意有些节点存储的字符串长度是小于对应匹配长度的,这样的节点是没有贡献的,因为其全部都是在 中出现过了的。
代码中找 endpos 等价类找的是其中最靠左的一个。实际上任意一个都可以。因为如果有一个与其最左端的 endpos 匹配长度不同,那么它的匹配长度也就超过了当前节点的最长子串,也就没有贡献了。
那么考虑对于一般的情况,我们转移到的节点的 endpos 必须满足在给定区间 内。也就是说,对应地,设当前匹配长度为 endpos 必须在区间 出现过。
那么我们如何维护节点的 endpos 集合呢?用线段树合并的经典 trick ,初始设置每一个节点的 endpos 是其刚插入的节点对应字符串的下标,然后对 SAM 拓扑排序之后依次线段树合并维护集合。
注意这里我们需要新建线段树,否则我们不能随时访问一个节点的 endpos 集合。因为一般的线段树合并会改变其原本结构。
于是注意线段树的空间不要开小。
设 为 中 前缀的最长后缀匹配长度,则答案就是:对于每一个节点 求
这个式子的意义就是 求出这个节点中与 中不能匹配的子串个数。减去的 就是可以匹配的最长长度。由于其后缀均连续,所以可以这样减。注意如果小于等于 意味着没有贡献。
总体思路:找出其所有公共子串再减去 的本质不同子串。而由于区间的限制,这里就需要用 线段树合并 来维护一个节点的 endpos 集合。
注意统计答案的时候需要到 的自动机上面来查询,对应的 endpos 什么的都是 上面的。就是上面提到的,用 的本质不同子串减去其公共的子串。
总时间复杂度:
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
typedef long long ll;
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
inline ll Max(ll x,ll y){return x>y?x:y;}
inline ll Min(ll x,ll y){return x<y?x:y;}
namespace SGT{
int ls[N<<4],rs[N<<4],tot,n;
inline void init(const int&len){n=len;}
void Ins(int &x,const int &pos,const int& l=1,const int& r=n){
if(!x)x=++tot;
if(l==r)return;
int mid=(l+r)>>1;
if(pos<=mid)Ins(ls[x],pos,l,mid);
else Ins(rs[x],pos,mid+1,r);
}
inline int Add(const int &x){int res=++tot;Ins(tot,x);return res;}
int merge(const int& x,const int& y){
if(!x||!y)return x|y;
int p=++tot;
ls[p]=merge(ls[x],ls[y]);
rs[p]=merge(rs[x],rs[y]);
return p;
}
bool query(const int &x,const int &L,const int &R,const int &l=1,const int &r=n){
if(!x||R<l||L>r)return false;
if(l>=L&&r<=R)return true;
int mid=(l+r)>>1;
return query(ls[x],L,R,l,mid)|query(rs[x],L,R,mid+1,r);
}
}
struct SAM{
int len[N],pa[N],ch[N][26],rt[N],tot,last,minr[N];
inline void clear(){tot=last=1;memset(ch[1],0,sizeof ch[1]);}
SAM(){clear();}
void Exd(const int &c,const int &pos=0){
int p=last,np=last=++tot;
minr[np]=len[np]=len[p]+1;
if(pos)rt[np]=::SGT::Add(pos);
memset(ch[np],0,sizeof ch[np]);
for(;p&&!ch[p][c];p=pa[p])ch[p][c]=np;
if(!p)pa[np]=1;
else{
int q=ch[p][c];
if(len[p]+1==len[q])pa[np]=q;
else{
int nq=++tot;len[nq]=len[p]+1;
pa[nq]=pa[q];pa[np]=pa[q]=nq;
minr[nq]=minr[q];memcpy(ch[nq],ch[q],sizeof ch[q]);
for(;p&&ch[p][c]==q;p=pa[p])ch[p][c]=nq;
}
}
}
void find(int &p,int &l,int &L,int &R,const int &c){
while(114514){
if(ch[p][c]&&::SGT::query(rt[ch[p][c]],L+l,R)){
++l;p=ch[p][c];return;
}
if(!l)return;
--l;if(l==len[pa[p]])p=pa[p];
}
}
ll calc(int p[]){
ll res=0;
for(int i=2;i<=tot;++i)
res+=Max(0,len[i]-Max(len[pa[i]],p[minr[i]]));
return res;
}
}S1,S2;
char S[500010],T[500010];
int p[1000010],c[500010],rk[1000010],Q;
int main(){
scanf("%s",S+1);
int len=strlen(S+1);
SGT::init(len);
for(int i=1;i<=len;++i)S1.Exd(S[i]-'a',i);
for(int i=1;i<=S1.tot;++i)++c[S1.len[i]];
for(int i=1;i<=len;++i)c[i]+=c[i-1];
for(int i=1;i<=S1.tot;++i)rk[--c[S1.len[i]]]=i;
for(int i=S1.tot;i;--i){S1.rt[S1.pa[rk[i]]]=SGT::merge(S1.rt[S1.pa[rk[i]]],S1.rt[rk[i]]);}
scanf("%d",&Q);
while(Q--){
S2.clear();
int l,r;
scanf("%s%d%d",T+1,&l,&r);
int Len=strlen(T+1),pp=1;
for(int i=1;i<=Len;++i){
p[i]=p[i-1];
S1.find(pp,p[i],l,r,T[i]-'a');
S2.Exd(T[i]-'a');
}
printf("%lld\n",S2.calc(p));
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?