【洛谷P4770】你的名字
题目
题目链接:https://www.luogu.com.cn/problem/P4770
小 A 被选为了 ION2018 的出题人,他精心准备了一道质量十分高的题目,且已经把除了题目命名以外的工作都做好了。
由于 ION 已经举办了很多届,所以在题目命名上也是有规定的,ION 命题手册规定:每年由命题委员会规定一个小写字母字符串,我们称之为那一年的命名串,要求每道题的名字必须是那一年的命名串的一个非空连续子串,且不能和前一年的任何一道题目的名字相同。
由于一些特殊的原因,小 A 不知道 ION2017 每道题的名字,但是他通过一些特殊手段得到了 ION2017 的命名串,现在小 A 有 \(Q\) 次询问:每次给定 ION2017 的命名串和 ION2018 的命名串,求有几种题目的命名,使得这个名字一定满足命题委员会的规定,即是 ION2018 的命名串的一个非空连续子串且一定不会和 ION2017 的任何一道题目的名字相同。
由于一些特殊原因,所有询问给出的 ION2017 的命名串都是某个串的连续子串,详细可见输入格式。
\(|S|,|T|\leq 5\times 10^5;\sum |T|\leq 10^6;Q\leq 10^5\)。
思路
SAM Visualizer 简直是 debug 神器!!!!11
设 \(n=|S|,m=|T|\)。
首先考虑 \(l=1,r=n\) 的部分分。也就是给定 \(S,T\),询问有多少个 \(T\) 本质不同的子串没有在 \(S\) 中出现过。
显然只需要求出有多少个 \(T\) 本质不同的子串在 \(S\) 中出现过,然后用本质不同子串数量减一下即可。
考虑枚举 \(T\) 的每一个位置 \(i\),不难发现以 \(i\) 结尾,且在 \(S\) 中出现过的 \(T\) 的子串一定是右端点为 \(i\) 的一段后缀。我们只需要求出每一个后缀的长度加起来即可。当然还需要保证本质不同。
对 \(S\) 和 \(T\) 分别建一个 SAM,然后考虑将 \(T\) 在 \(S\) 的后缀自动机上跑匹配。我们知道 parent 树上,一个节点 \(x\) 的父亲表示 \(x\) 所代表等价类最短串删去第一个字符后所代表的等价类。这玩意和 AC 自动机的 fail 十分相似。
假设我们枚举到 \(T\) 的第 \(i\) 位,现在在 \(S\) 的后缀自动机上匹配到的点为 \(x\),那么我们可以不断跳 \(x\) 在 parent 树上的父亲,直到 \(x\) 在后缀自动机上有 \(T_i\) 这条出边为止,那么直接把 \(x\) 赋值为出边所连接的点编号即可。显然这一个点就是与 \(T\) 在 \(i\) 处结尾的子串匹配的等价类。匹配的长度为 \(\text{len}_{\text{fa}[x]}+1\)。
那么我们照样在 \(T\) 的后缀自动机上找到 \(T[1:i]\) 所在的等价类,然后把匹配长度记录下来,最后 topsort 一次计算答案即可。
时间复杂度是喜闻乐见的 \(O(n+m)\) 的。
接下来考虑回原题。我们肯定无法快速得到 \(S\) 的一个子串的 SAM,但是观察我们在 \(S\) 的 SAM 上的操作只有以下三种:
- 跳 parent 树匹配。
- 判断一个点 \(x\) 是否有字符 \(c\) 的出边。
- 用点 \(\text{len}_x\) 更新 \(T\) 的后缀自动机的答案。
其中跳 parent 树是不受区间影响的,复杂度只和 \(\sum |T|\) 有关。
判断一个点 \(x\) 是否有字符 \(c\) 的出边则增加一个一个要求:去往的点的 \(\text{endpos}\cap [l,r]\neq \emptyset\)。这个直接可持久化线段树合并维护 \(\text{endpos}\) 即可。
用点 \(\text{len}_x\) 更新 \(T\) 的后缀自动机的答案,因为有了区间限制,我们可能取到的串有一部分前缀会超出区间范围。但是也比较容易处理。我们在线段树上维护区间最大值,要求匹配串长度时就查询 \([l,r]\) 的最大值,因为在一个等价类中,我们要保证能往前选择更多,在右端点肯定不超过范围的前提下,右端点越后能去到的长度越长。具体的,记 \(p\) 为 \(\text{endpos}\cap [l,r]\) 集合中的最大值,能匹配的长度则为 \(\min(\text{len}_{\text{fa}[x]+1,p-l+1})\)。
有一点需要注意的是,取了 \(\min\) 之后,能匹配的长度可能就比 \(x\) 等价类中最短子串的长度更短了,此时我们依然需要继续跳 parent 树,因为 \(\text{fa}[x]\) 可能会有更后的 \(\text{endpos}\)。
那么我们就解决了区间的问题。时间复杂度 \(O((n+\sum m)\log n)\)。
代码
因为一个傻逼错误调了一个下午。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1000010,LG=20;
int n,m,Q,leng,rt[N];
char s[N],t[N];
ll ans;
struct SegTree
{
int tot,lc[N*LG*4],rc[N*LG*4],maxp[N*LG*4];
int update(int x,int l,int r,int k,int pos)
{
if (!x) x=++tot;
if (l==r) { maxp[x]=max(maxp[x],pos); return x; }
int mid=(l+r)>>1;
if (k<=mid) lc[x]=update(lc[x],l,mid,k,pos);
else rc[x]=update(rc[x],mid+1,r,k,pos);
maxp[x]=max(maxp[lc[x]],maxp[rc[x]]);
return x;
}
int merge(int x,int y)
{
if (!x || !y) return x|y;
int p=++tot;
maxp[p]=max(maxp[x],maxp[y]);
lc[p]=merge(lc[x],lc[y]);
rc[p]=merge(rc[x],rc[y]);
return p;
}
int query(int x,int l,int r,int ql,int qr)
{
if (ql<=l && qr>=r) return maxp[x];
int mid=(l+r)>>1,res=0;
if (ql<=mid) res=max(res,query(lc[x],l,mid,ql,qr));
if (qr>mid) res=max(res,query(rc[x],mid+1,r,ql,qr));
return res;
}
}seg;
struct SAM
{
int last,tot,len[N],fa[N],ch[N][26],res[N],wyctql[N],xjqtql[N];
void init()
{
for (int i=0;i<=tot;i++)
{
len[i]=fa[i]=wyctql[i]=xjqtql[i]=res[i]=0;
for (int j=0;j<26;j++) ch[i][j]=0;
}
last=tot=1;
}
void ins(int c,int k)
{
int p=last,np=++tot;
last=np; len[np]=len[p]+1;
if (k!=-1) rt[np]=seg.update(rt[np],1,n,k,k);
for (;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if (!p) fa[np]=1;
else
{
int q=ch[p][c];
if (len[q]==len[p]+1) fa[np]=q;
else
{
int nq=++tot;
fa[nq]=fa[q]; len[nq]=len[p]+1;
for (int i=0;i<26;i++) ch[nq][i]=ch[q][i];
fa[q]=fa[np]=nq;
for (;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
}
void topsort(bool type)
{
for (int i=1;i<=tot;i++) xjqtql[len[i]]++;
for (int i=1;i<=tot;i++) xjqtql[i]+=xjqtql[i-1];
for (int i=1;i<=tot;i++) wyctql[xjqtql[len[i]]--]=i;
for (int i=tot;i>=1;i--)
{
int x=wyctql[i];
if (!type) rt[fa[x]]=seg.merge(rt[fa[x]],rt[x]);
else
{
res[fa[x]]=max(res[fa[x]],res[x]);
res[x]=min(res[x],len[x]);
if (x>1) ans+=len[x]-max(len[fa[x]],res[x]);
}
}
}
int find(int x,int c,int l,int r)
{
for (;x;x=fa[x],leng=len[x])
{
if (!ch[x][c]) continue;
int p=seg.query(rt[ch[x][c]],1,n,l,r);
if (p && p-l+1>len[fa[ch[x][c]]])
{
leng=min(leng+1,p-l+1);
return ch[x][c];
}
}
return 1;
}
}sam1,sam2;
void solve(int l,int r)
{
leng=0;
for (int i=1,p1=1,p2=1;i<=m;i++)
{
p1=sam1.find(p1,t[i]-'a',l,r);
p2=sam2.ch[p2][t[i]-'a'];
sam2.res[p2]=leng;
}
}
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
sam1.init();
for (int i=1;i<=n;i++)
sam1.ins(s[i]-'a',i);
sam1.topsort(0);
scanf("%d",&Q);
while (Q--)
{
int l,r; ans=0;
scanf("%s%d%d",t+1,&l,&r);
m=strlen(t+1);
sam2.init();
for (int i=1;i<=m;i++)
sam2.ins(t[i]-'a',-1);
solve(l,r);
sam2.topsort(1);
printf("%lld\n",ans);
}
return 0;
}