后缀自动机小结

废话

花了大半天才把这东西学下来,现在还是感觉挺仙的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 1469

满足要求的图是造出来了,但是,点数太多了,而且造这个东西需要 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 0234

对于状态之间的边,我们用 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 x1 开始,这些 B B B 的后缀和 B B B 不在同一个类中,设长度为 x − 1 x-1 x1 B B B 的后缀为 C C C,设 C C C 所在的状态为 S ′ S' S,那么 S S S 的后缀链接就指向 S ′ S' S

为了方便,下面称这个后缀 C C C 为最早的不同类后缀。

显然的,有这样一些性质:

  1. 状态 S S S 内子串的 e n d p o s endpos endpos S ′ S' S 内子串的 e n d p o s endpos endpos 的子集。
  2. C C C S ′ S' S 内的最长串
  3. 后缀链接构成一棵树,根就是状态 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]差异   题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值