废话
花了大半天才把这东西学下来,现在还是感觉挺仙的qwq。
顺便说一下,我在网上找到的个人认为最好的文章是这两篇:(学习概念)hihocoder,(整体入门)大佬翻译的俄文文章。
当然看我这篇也行啦qwq,不过有些地方可能讲不太清楚?(语文能力有限啊qwq)
正题
定义: 对给定字符串 s s s 的后缀自动机是一个最小化确定有限状态自动机,它能够接收字符串s的所有后缀。
不用太纠结上面这句话,往下看就明白了。
引子
我们希望采用一种有向无环图的结构来存储一个字符串,点表示某种状态,边表示字符,并且只有一个起点。
最重要的,这个字符串的每一个子串都可以表示为某条从起点出发的路径
。
显然,把这个字符串的所有后缀都插入到一棵字典树中,就可以得到一个满足要求的图。比如说字符串为
a
b
a
b
a
a
ababaa
ababaa 时,图长这样(起点是
1
1
1):
方向大家都懂,我忘记画了……
比如要找子串 b a b bab bab,对应的路径就是 1 → 4 → 6 → 9 1\to 4\to 6\to 9 1→4→6→9。
满足要求的图是造出来了,但是,点数太多了,而且造这个东西需要 O ( n 2 ) O(n^2) O(n2) 的时间,于是,后缀自动机出现了,后缀自动机就是一种满足要求的图,并且点数最少,造出来的时间也只需要 O ( n ) O(n) O(n)。
后缀自动机
首先需要了解一些概念。
endpos
假设 s ′ s' s′ 是 s s s 的一个子串,那么 e n d p o s ( s ′ ) endpos(s') endpos(s′) 表示 s s s 中所有 s ′ s' s′ 的结束位置,依然用上面的 a b a b a a ababaa ababaa 为例,子串 b a ba ba 在其中出现过两次,结束位置分别为 3 , 5 3,5 3,5,所以 e n d p o s ( a b ) = { 3 , 5 } endpos(ab)=\{3,5\} endpos(ab)={3,5}。
对于
e
n
d
p
o
s
endpos
endpos 完全一样的子串,我们称之为 终点等价
。于是我们可以将所有子串分为若干个终点等价类
。
比如说,对于字符串 a b a b a a ababaa ababaa, e n d p o s endpos endpos 为 { 4 } \{4\} {4} 的终点等价类包含 a b a b , b a b abab,bab abab,bab 两个子串,但是不包含 a b ab ab,因为它的 e n d p o s endpos endpos 是 { 2 , 4 } \{2,4\} {2,4}。
这东西有好几个性质,虽然都很容易明白就是了。
-
性质1: 设 A A A 为 s s s 的子串, B B B 为 A A A 的某个后缀,那么有: e n d p o s ( A ) ⊆ e n d p o s ( B ) endpos(A)\subseteq endpos(B) endpos(A)⊆endpos(B)。
证明: s s s 中出现 A A A 的地方必定会出现 B B B,而出现 B B B 的地方不一定会出现 A A A,所以 A A A 的 e n d p o s endpos endpos 是 B B B 的 e n d p o s endpos endpos 的子集。
例: s = a b a b a a , A = b a , B = a s=ababaa,A=ba,B=a s=ababaa,A=ba,B=a,则 e n d p o s ( A ) = { 3 , 5 } , e n d p o s ( B ) = { 1 , 3 , 5 , 6 } endpos(A)=\{3,5\},endpos(B)=\{1,3,5,6\} endpos(A)={3,5},endpos(B)={1,3,5,6},满足 e n d p o s ( A ) ⊆ e n d p o s ( B ) endpos(A)\subseteq endpos(B) endpos(A)⊆endpos(B)。 -
性质2: 对于 s s s 的两个子串 A , B A,B A,B( A A A 的长度比 B B B 短),假如 A A A 和 B B B 在同一个终点等价类中,那么 A A A 一定是 B B B 的后缀。
证明: 这个很显然啊……不是后缀的话 e n d p o s endpos endpos 怎么能相等。 -
性质3: 设一个终点等价类中最短的子串为 A A A,长度为 x x x,最长的为 B B B,长度为 y y y,那么这个类中所有子串都是 B B B 的后缀并且长度恰好取遍 [ x , y ] [x,y] [x,y]。
证明:
都是 B B B 的后缀就不用说了,要证的是为什么取遍 [ x , y ] [x,y] [x,y] 整个连续的区间。
首先,可以知道的是 e n d p o s ( A ) = e n d p o s ( B ) endpos(A)=endpos(B) endpos(A)=endpos(B)。
假设有一个长度为 z ∈ ( x , y ) z\in(x,y) z∈(x,y) 的 B B B 的后缀,我们称它为 C C C,那么就有 e n d p o s ( B ) ⊆ e n d p o s ( C ) ⊆ e n d p o s ( A ) endpos(B)\subseteq endpos(C)\subseteq endpos(A) endpos(B)⊆endpos(C)⊆endpos(A),所以 e n d p o s ( C ) = e n d p o s ( A ) = e n d p o s ( B ) endpos(C)=endpos(A)=endpos(B) endpos(C)=endpos(A)=endpos(B)。
所以长度为 [ x , y ] [x,y] [x,y] 的 B B B 的后缀都在这个类中。
而在后缀自动机中,一个终点等价类就是其中的一个点,后面会证明这样做点数不超过 2 n 2n 2n。
以及,在后缀自动机中,我们称一个点为一个状态。起点表示的类里面是空子串, e n d p o s endpos endpos 为 1 1 1 到 n n n。
还是以 a b a b a a ababaa ababaa 为例,终点等价类有这些:
{
1
,
3
,
5
,
6
}
:
a
\{1,3,5,6\}:a
{1,3,5,6}:a
{
2
,
4
}
:
b
,
a
b
\{2,4\}:b,ab
{2,4}:b,ab
{
3
,
5
}
:
b
a
,
a
b
a
\{3,5\}:ba,aba
{3,5}:ba,aba
{
6
}
:
a
a
,
b
a
a
,
a
b
a
a
,
b
a
b
a
a
,
a
b
a
b
a
a
\{6\}:aa,baa,abaa,babaa,ababaa
{6}:aa,baa,abaa,babaa,ababaa
{
4
}
:
b
a
b
,
a
b
a
b
\{4\}:bab,abab
{4}:bab,abab
{
5
}
:
b
a
b
a
,
a
b
a
b
a
\{5\}:baba,ababa
{5}:baba,ababa
造成后缀自动机就是(为了方便,将每个状态中最长的子串标在了旁边):
可以发现,这个后缀自动机满足我们一开始的要求:每一个子串都可以表示为某条从起点出发的路径
,其中,起点是状态
0
0
0。
比如说,子串 b a b bab bab 对应的路径就是 0 → 2 → 3 → 4 0\to 2\to 3\to 4 0→2→3→4。
对于状态之间的边,我们用 n e x t next next 数组来表示,如 n e x t [ 1 ] [ b ] = 2 next[1][b]=2 next[1][b]=2,表示状态 1 1 1 通过 b b b 转移到状态 2 2 2(下面会沿用这种表述方式)。
可以发现,假如有
n
e
x
t
[
A
]
[
c
]
=
B
next[A][c]=B
next[A][c]=B,那么状态
B
B
B 中一定包含状态A中的每个子串后面加字符c得到的新子串
,比如
n
e
x
t
[
1
]
[
b
]
=
2
next[1][b]=2
next[1][b]=2,状态
2
2
2 就包含了状态
1
1
1 中的所有子串(即
a
a
a)后面加
b
b
b 得到的新子串
a
b
ab
ab。
后缀链接
这个东西是用来辅助构造后缀自动机的。
对于一个状态 S S S,设这个类中最短的子串为 A A A,长度为 x x x,最长的子串为 B B B,长度为 y y y,上面说过,这个类中的所有子串都是 B B B 的后缀且长度覆盖 [ x , y ] [x,y] [x,y]。
这意味着,从长度为 x − 1 x-1 x−1 开始,这些 B B B 的后缀和 B B B 不在同一个类中,设长度为 x − 1 x-1 x−1 的 B B B 的后缀为 C C C,设 C C C 所在的状态为 S ′ S' S′,那么 S S S 的后缀链接就指向 S ′ S' S′。
为了方便,下面称这个后缀 C C C 为最早的不同类后缀。
显然的,有这样一些性质:
- 状态 S S S 内子串的 e n d p o s endpos endpos 是 S ′ S' S′ 内子串的 e n d p o s endpos endpos 的子集。
- C C C 是 S ′ S' S′ 内的最长串
- 后缀链接构成一棵树,根就是状态 0 0 0。
还还还是用
a
b
a
b
a
a
ababaa
ababaa 作为例子,构建出后缀链接的树就长这样:
为了方便,我们令
l
i
n
k
[
x
]
link[x]
link[x] 表示状态
x
x
x 的后缀链接,特别的,
l
i
n
k
[
0
]
=
−
1
link[0]=-1
link[0]=−1。
小结
梳理一下上面的知识,然后再讲如何构造后缀自动机。
- 字符串 s s s 的子串可以分成若干个终点等价类
- 每个终点等价类是后缀自动机上的一个状态
- 每个状态包含了最长串的一段后缀
- 一个状态的后缀链接指向 最长的 不在该状态内 的后缀 所在的状态
构造
这个构造算法是在线的,也就是将字符串中的字符一个一个丢进来构造,一开始只有状态 0 0 0。
为了方便,设 l o n g e s t [ A ] longest[A] longest[A] 和 l e n [ A ] len[A] len[A] 分别表示状态 A A A 中的最长子串以及它的长度。
假设此时已经构造好了字符串 s s s(长度为 n n n),现在要在末尾新增一个字符 c c c,我们一步一步来看:
- 设 l a s t last last 表示包含子串 s s s 的状态,一开始 l a s t = 0 last=0 last=0
- 加了一个新字符,肯定会多一种状态,这个状态只包含子串 s + c s+c s+c,设这个状态为 n o w now now,那么有 l e n [ n o w ] = l e n [ l a s t ] + 1 len[now]=len[last]+1 len[now]=len[last]+1(即 n + 1 n+1 n+1)
- 设 p = l a s t p=last p=last,让 p p p 沿着后缀链接走,假如 p p p 没有通过字符 c c c 的转移(即 n e x t [ p ] [ c ] = 0 next[p][c]=0 next[p][c]=0),那么就让 n o w now now 成为它的转移,如果有,就停在这里。
- 接下来有两种结果:1、 p p p 一直走到 − 1 -1 −1;2、 p p p 停在了某处。假如是结果 1 1 1 的话,那么这次构造就做完了,直接return。
- 假如是结果 2 2 2,就需要分类讨论,设 n e x t [ p ] [ c ] = q next[p][c]=q next[p][c]=q。
- 情况 1 1 1: l e n [ p ] + 1 = l e n [ q ] len[p]+1=len[q] len[p]+1=len[q]。此时让 l i n k [ n o w ] = q link[now]=q link[now]=q 即可,然后return,因为很显然 l o n g e s t [ q ] longest[q] longest[q] 是 n o w now now 的最早的不同类后缀。
- 情况 2 2 2: l e n [ p ] + 1 < l e n [ q ] len[p]+1<len[q] len[p]+1<len[q]。这种情况比较复杂,对于状态 q q q 中长度大于 l e n [ p ] + 1 len[p]+1 len[p]+1 的部分,他们是状态 p p p 通过字符 c c c 转移不到的,也就是说,只有 ≤ l e n [ p ] + 1 \leq len[p]+1 ≤len[p]+1 的部分可以转移到,而能转移到意味着这部分的子串的 e n d p o s endpos endpos 集合要多一个元素—— n + 1 n+1 n+1,既然多了一个元素,那么就和 > l e n [ p ] + 1 >len[p]+1 >len[p]+1 的部分的 e n d p o s endpos endpos 不一样了,所以要新开一个状态 c l o n e clone clone,完全继承状态 q q q 的信息,除了 l e n len len 要等于 l e n [ p ] + 1 len[p]+1 len[p]+1。
- 然后,还需要将一些
原来通过字符c转移到状态q
的状态 A A A 的 n e x t [ A ] [ c ] next[A][c] next[A][c] 改成 c l o n e clone clone。具体就是让 p p p 继续沿着后缀链接走,将遇到的都改了(因为它们的endpos都增加了一个 n + 1 n+1 n+1),直到走到某个状态 B B B,满足 B = 0 B=0 B=0 或 n e x t [ B ] [ C ] next[B][C] next[B][C] 不等于 q q q 就停止。因为如果遇到了一个 n e x t next next 不等于 q q q 的,后面就不会再遇到等于 q q q 的了,这个结合上面说过的一个状态内的子串是最长子串的一段后缀
来理解一下就好。 - 最后,让 l i n k [ n o w ] = c l o n e link[now]=clone link[now]=clone 以及 l i n k [ q ] = c l o n e link[q]=clone link[q]=clone 即可。
最后就是代码了:
struct state{
int len,link;
map<char,int>next;
state():len(0),link(0){}
}st[maxn];
int id=0,last=0,p,q,clone;
void extend(char x)//新加入字符x
{
int now=++id;
st[now].len=st[last].len+1;
for(p=last;p!=-1&&!st[p].next.count(x);p=st[p].link)st[p].next[x]=now;
if(p!=-1)
{
q=st[p].next[x];
if(st[p].len+1==st[q].len)st[now].link=q;
else
{
clone=++id;
st[clone]=st[q];st[clone].len=st[p].len+1;
for(;p!=-1&&st[p].next[x]==q;p=st[p].link)st[p].next[x]=clone;
st[q].link=st[now].link=clone;
}
}
last=now;
}
可以发现, n e x t next next 数组是用 m a p map map 实现的,所以时间复杂度为 O ( n × l o g ( k ) ) O(n\times log(k)) O(n×log(k)), k k k 是字符集大小。如果 k k k 比较小,可以直接开数组而不用 m a p map map。
可以发现,每次加入新字符时,创建的状态最多两个,也就是说,最后的后缀自动机的状态数不超过 2 n 2n 2n,这就解决了上面埋下的疑问。
不过具体的时间复杂度证明可以参考一开始的那篇俄文文章,写得超级好。(不过长到离谱就是了……)
例题
这道题也就是要我们求出每个状态的 e n d p o s endpos endpos 集的大小 s i z e [ x ] size[x] size[x],然后求出 l e n [ x ] × s i z e [ x ] len[x]\times size[x] len[x]×size[x] 的最大值。
这个东西很好求, e n d p o s endpos endpos 集的大小也就是子串在字符串内的出现次数,要知道一个状态 A A A 内的子串的出现次数,只需要找到有多少个 包含字符串前缀的状态 能通过后缀链接走到 A A A 即可。
因为一个子串可以表示为字符串的某个前缀的某个后缀,而后缀链接可以帮助判断 A A A 是不是 B B B 的后缀,那么只需要看 A A A 是多少个前缀的后缀即可。具体就是在extend函数里加一句 s i z e [ n o w ] = 1 size[now]=1 size[now]=1,然后把后缀链接树造出来,统计一下每个节点的子树内的 s i z e size size 之和即可。
代码如下:
#include <cstdio>
#include <cstring>
#include <map>
#include <algorithm>
using namespace std;
#define maxn 2000010
int n;
char s[maxn];
struct state{
int len,link;
map<char,int>next;
state():len(0),link(0){}
}st[maxn];
int id=0,last=0,p,q,clone;
int size[maxn];
void extend(char x)
{
int now=++id;
st[now].len=st[last].len+1;size[now]=1;
for(p=last;p!=-1&&!st[p].next.count(x);p=st[p].link)st[p].next[x]=now;
if(p!=-1)
{
q=st[p].next[x];
if(st[p].len+1==st[q].len)st[now].link=q;
else
{
clone=++id;
st[clone]=st[q];st[clone].len=st[p].len+1;
for(;p!=-1&&st[p].next[x]==q;p=st[p].link)st[p].next[x]=clone;
st[q].link=st[now].link=clone;
}
}
last=now;
}
struct edge{int y,next;};
edge e[maxn];
int first[maxn],len=0;
void buildroad(int x,int y)
{
e[++len]=(edge){y,first[x]};
first[x]=len;
}
long long ans=0;
void dfs(int x)
{
for(int i=first[x];i;i=e[i].next)dfs(e[i].y),size[x]+=size[e[i].y];
if(size[x]!=-1)ans=max(ans,1ll*len[x]*size[x]);
}
int main()
{
scanf("%s",s+1);n=strlen(s+1);
st[0].link=-1;
for(int i=1;i<=n;i++)extend(s[i]);
for(int i=1;i<=id;i++)buildroad(st[i].link,i);
dfs(0);printf("%lld",ans);
}
题表
遇到好题可能还会更新一下qwq?
SP8222 NSUBSTR - Substrings 题解
[SDOI2016]生成魔咒 题解
[TJOI2015]弦论 题解
[HAOI2016]找相同字符 题解
[AHOI2013]差异 题解