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;
}
又有思维难度又有代码难度,这就是后缀自动机。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律