题目
给你一个字符串,问所有长度为
m
m
m的字符串之中,对于子串
i
i
i,和它相似的子串分别是什么。
“相似”的概念:两个字符串至多有一个位置的字符不同。
n
≤
1
e
5
n\leq 1e5
n≤1e5
正解
由于比赛的时候基本上都在刚T1,所以这题没有干过。
各种暴力,大概都是从快速地判断子串相等入手。
但是正解用到了一个新的性质:对于字符串
S
S
S和
T
T
T,若
l
c
p
(
S
,
T
)
+
l
c
s
(
S
,
T
)
≥
m
−
1
lcp(S,T)+lcs(S,T)\geq m-1
lcp(S,T)+lcs(S,T)≥m−1,则他俩相似。
原因不解释。
顺着这个思路,很容易(可能)想到建后缀树和前缀树。
建树直接跑SAM,SAM的
f
a
i
l
fail
fail树就是反串的后缀树。
对于两个字符串
u
u
u和
v
v
v(这个是字符串的编号,并不是字符串的端点):
它们在后缀树上的
L
C
A
LCA
LCA的深度(SAM中的
l
e
n
len
len),便是它们
l
c
p
lcp
lcp。
那么我们考虑在后缀树的
L
C
A
LCA
LCA处计算贡献:以某个点为根的时候,
l
c
p
lcp
lcp定下来了。枚举属于两个不同子树中的点,通过前缀树计算他们的
l
c
s
lcs
lcs,如果
l
c
s
≥
m
−
1
−
l
c
p
lcs\geq m-1-lcp
lcs≥m−1−lcp,它们就是合法的一对。
思考
l
c
p
lcp
lcp确定的时候,对于某个点
u
u
u,合法的
v
v
v满足什么。
u
u
u和
v
v
v在前缀树上的
L
C
A
LCA
LCA的深度大于某个值,所以合法的
v
v
v在某棵子树内。
接下来才是具体做法:
考虑dsu on tree(可以上网搜,本质上和启发式合并差不多),先处理完轻儿子,将轻儿子的信息清空;处理重儿子,然后将重儿子的信息继承过来,暴力每个轻儿子,维护信息。
考虑
A
A
A和
B
B
B两个树连通块合并,前者比后者大,按照启发式合并的思想,暴力枚举
B
B
B内的点。
用个数据结构来维护一下在前缀树中的dfs序区间内,对应的后缀树上的点出现在
A
A
A的点有多少个。支持单点加,区间查就好了。
对于点
u
u
u,倍增算出合法的
v
v
v在哪个节点的子树内,然后在数据结构上查。
整个
B
B
B处理完之后,就合并入
A
A
A中,具体来说就是将他们每个点往数据结构里加。
最后,在清空信息的时候,不要忘了数据结构上的信息也要一同清空。
然而,我们只是做到了
(
u
,
v
)
(u,v)
(u,v)的贡献挂在
u
u
u上(
u
∈
B
u \in B
u∈B),并没有挂在
v
v
v上。
于是我们再维护一个数据结构。这个支持区间加,单点查:对于点
u
u
u,求出合法的
v
v
v在哪个节点的子树内,然后在数据结构上对应的区间中加。在最后输出答案的时候单点查加上贡献。
不过要注意一下:当
u
u
u从集合
B
B
B中进入集合
A
A
A中,它身上本来是不带贡献的(指
u
∈
A
u\in A
u∈A时与某些
B
B
B产生的贡献),但是它在某次修改中被连带着“误改”过。这怎么办呢?
如果用线段树,可以用粗暴下传标记的方式来解决这个问题。
其实没有这个必要。既然是“误改”的,那就在答案中剪掉嘛……(另外记得,清空信息的时候,单点查询,将贡献计入答案)
所以,只需要用树状数组就可以解决这个问题。
总时间复杂度
O
(
n
lg
2
n
)
O(n\lg^2 n)
O(nlg2n)
另外,C_C讲题的时候讲了个在SA上分治的做法。具体就是在区间中找到 h e i g h t height height最小的位置,将区间分成两半。这个方法和我上面将的这个方法本质上并没有多大区别,因为众所周知,后缀数组就是后缀树的dfs序, h e i g h t height height就是相邻两个点的 L C A LCA LCA深度。
代码
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100010
#define ll long long
int n,m;
char str[N];
struct Node{
Node *c[26],*fail;
int len;
} d[N*4],*S1,*S2,*T;
int cnt;
inline void insert(char ch,Node *S){
Node *nw=&d[++cnt];
nw->len=T->len+1;
Node *p=T;
for (;p && !p->c[ch-'a'];p=p->fail)
p->c[ch-'a']=nw;
if (!p)
nw->fail=S;
else{
Node *q=p->c[ch-'a'];
if (q->len==p->len+1)
nw->fail=q;
else{
Node *clone=&d[++cnt];
memcpy(clone,q,sizeof *q);
clone->len=p->len+1;
q->fail=nw->fail=clone;
for (;p && p->c[ch-'a']==q;p=p->fail)
p->c[ch-'a']=clone;
}
}
T=nw;
}
int id1[N],id2[N],re[N*4];
struct EDGE{
int to;
EDGE *las;
} e[N*4];
int ne;
EDGE *last[N*4];
int rt1,rt2;
int fa[N*4][20];
int in[N*4],out[N*4],tot;
int dep[N*4],siz[N*4],hs[N*4];
void init(int x){
for (int i=1;1<<i<=dep[x];++i)
fa[x][i]=fa[fa[x][i-1]][i-1];
in[x]=++tot;
for (EDGE *ei=last[x];ei;ei=ei->las)
init(ei->to);
out[x]=tot;
}
void getsiz(int x){
siz[x]=1;
for (EDGE *ei=last[x];ei;ei=ei->las){
getsiz(ei->to);
siz[x]+=siz[ei->to];
if (siz[ei->to]>siz[hs[x]])
hs[x]=ei->to;
}
}
int find(int x,int tar){
if (tar>dep[x])
return 0;
tar=max(tar,0);
for (int i=19;i>=0;--i)
if (dep[fa[x][i]]>=tar)
x=fa[x][i];
return x;
}
int _t[N*2],s[N*2];
inline void add(int x,int c,int *t=_t){
for (;x<=tot;x+=x&-x)
t[x]+=c;
}
inline int query(int x,int *t=_t){
int res=0;
for (;x;x-=x&-x)
res+=t[x];
return res;
}
ll sum,ans[N];
void scan(int x,int lim){
if (re[x]!=-1){
int y=find(id2[re[x]+m-1],lim);
if (y){
ans[re[x]]+=query(out[y])-query(in[y]-1);
add(in[y],1,s),add(out[y]+1,-1,s);
}
}
for (EDGE *ei=last[x];ei;ei=ei->las)
scan(ei->to,lim);
}
void insert(int x,int c){
if (re[x]!=-1){
add(in[id2[re[x]+m-1]],c);
ans[re[x]]-=c*query(in[id2[re[x]+m-1]],s);
}
for (EDGE *ei=last[x];ei;ei=ei->las)
insert(ei->to,c);
}
void dfs(int x){
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=hs[x]){
dfs(ei->to);
insert(ei->to,-1);
}
if (hs[x])
dfs(hs[x]);
if (re[x]!=-1){
int y=find(id2[re[x]+m-1],m-1-dep[x]);
if (y){
ans[re[x]]+=query(out[y])-query(in[y]-1);
add(in[y],1,s),add(out[y]+1,-1,s);
}
add(in[id2[re[x]+m-1]],1);
ans[re[x]]-=query(in[id2[re[x]+m-1]],s);
}
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=hs[x]){
scan(ei->to,m-1-dep[x]);
insert(ei->to,1);
}
}
int main(){
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
scanf("%d%d%s",&n,&m,str);
T=S1=&d[++cnt];
memset(re,255,sizeof re);
for (int i=n-1;i>=0;--i){
insert(str[i],S1),id1[i]=T-d;
re[T-d]=(i+m-1<n?i:-1);
}
T=S2=&d[++cnt];
for (int i=0;i<n;++i)
insert(str[i],S2),id2[i]=T-d;
dep[0]=-1;
for (int i=1;i<=cnt;++i){
dep[i]=d[i].len;
if (d[i].fail==NULL)
continue;
fa[i][0]=d[i].fail-d;
e[ne]={i,last[fa[i][0]]};
last[fa[i][0]]=e+ne++;
}
rt1=S1-d,rt2=S2-d;
init(rt2);
getsiz(rt1);
dfs(rt1);
for (int i=0;i+m-1<n;++i)
ans[i]+=query(in[id2[i+m-1]],s);
for (int i=0;i+m-1<n;++i)
printf("%d ",ans[i]);
return 0;
}
总结
其实这题并不是很难吧,但是细节有点多,害我搞了一天(应该跟早上边打程序边学习政治有关系,所以得到教训:调试的时候可以分心,打程序的时候尽量不要分心,不然细节很容易挂)
然后就是dsu on tree这个东西,说实话本质上是个用脚趾头都能想到的启发式合并。不过也应该要掌握这个概念,不要光想着尽管和它差不多的启发式合并。
最后:我发现我现在真是越来越不能打SA了。SA虽然思想简单,但细节遍地都是,极其难写。半年还是一年来见到SA我都会用SAM代替。然而,
SA是我必须要度过的难关,因为——SAM的空间不是那么好承受啊(