SAM 笔记

后缀自动机 SAM \text{SAM} SAM .

仅为学习笔记,思路会跟博客 后缀自动机(SAM)学习笔记 - zjp_shadow - 博客园 比较类似 .

定义 e n d p o s ( t ) endpos(t) endpos(t) 为子串 t t t s s s 中出现过的所有结束位置的并 .

阅读下文时建议将 状态 看成是 e n d p o s endpos endpos 的一个集合 .

有时也会用 parent树 \text{parent树} parent 来代替 后缀树 \text{后缀树} 后缀树 .

Lemma

对于两个子串 s 1 s1 s1 s 2 s2 s2 ,若 ∣ s 1 ∣ ≤ ∣ s 2 ∣ |s1|\le |s2| s1s2 ,则 s 1 s1 s1 s 2 s2 s2 的后缀当且仅当 e n d p o s ( s 1 ) ⊇ e n d p o s ( s 2 ) endpos(s1)\supseteq endpos(s2) endpos(s1)endpos(s2) s 1 s1 s1 不是 s 2 s2 s2 的后缀当且仅当 e n d p o s ( s 1 ) ∩ e n d p o s ( s 2 ) = ∅ endpos(s1)\cap endpos(s2)=\empty endpos(s1)endpos(s2)= .

包含的前提保证 s 2 s2 s2 出现的位置都会有 s 1 s1 s1 出现,由结尾位置相同并且 ∣ s 1 ∣ ≤ ∣ s 2 ∣ |s1|\le |s2| s1s2 可知 s 1 s1 s1 s 2 s2 s2 后缀 .

反之即 e n d p o s ( s 1 ) ∩ e n d p o s ( s 2 ) = ∅ endpos(s1)\cap endpos(s2) = \empty endpos(s1)endpos(s2)= .

Characters

s = aabbabd \text{s}=\text{aabbabd} s=aabbabd 为例 .

  1. SAM \text{SAM} SAM 中的一个状态包含的子串都具有相同的 e n d p o s endpos endpos ,并且它们互为后缀 .

一个状态指的是从起点出发到这个点的所有路径组成的子串的并 .

例子中状态 4 4 4 { bb  ,  aabb  ,  abb } \{\text{bb} \ , \ \text{aabb}\ , \ \text{abb}\} {bb , aabb , abb} .

对于一个状态 st \text{st} st , 记 S ( st ) S(\text{st}) S(st) 为状态 4 4 4 的所有子串集合, l ( st ) l(\text{st}) l(st) 为其中最长的子串, s h ( st ) sh(\text{st}) sh(st) 为其中最短的子串 .

例子中状态 4 4 4 l ( 4 ) = aabb l(4)=\text{aabb} l(4)=aabb s h ( 4 ) = bb sh(4)=\text{bb} sh(4)=bb .

  1. 对于任意 t ∈ S ( st ) \text{t}\in S(\text{st}) tS(st) ,都有 e n d p o s ( t ) ⊇ e n d p o s ( l ( st ) ) endpos(t)\supseteq endpos(l(\text{st})) endpos(t)endpos(l(st)) .
  2. S ( st ) S(\text{st}) S(st) 包含的是 l ( st ) l(\text{st}) l(st) 的一系列后缀 .

后缀链接

考虑状态 7 7 7 S ( 7 ) = { aabbab  ,  abbab  ,  bbab  ,  bab } S(7)=\{\text{aabbab}\ , \ \text{abbab} \ , \ \text{bbab} \ , \ \text{bab}\} S(7)={aabbab , abbab , bbab , bab} ,接下来的一个后缀 a b ab ab 由于 e n d p o s ( ab ) = { 3   ,   6 } endpos(\text{ab})=\{3\ , \ 6\} endpos(ab)={3 , 6} ,不完全等于状态 7 7 7 ( e n d p o s ( t   ∣   t ∈ S ( 7 ) ) = 6 endpos(t\ |\ t\in S(7))=6 endpos(t  tS(7))=6) ,因此 ab \text{ab} ab 不在状态 7 7 7 中,称这种情况叫做 “断掉” .

l ( st ) l(\text{st}) l(st) 的某个后缀在新的位置中出现时,就会 “断掉” , 这个后缀 s s s 就会属于一个新的状态 .

上面的例子中 ab \text{ab} ab 属于 8 8 8 ,接着考虑 b \text{b} b 会发现 e n d p o s ( b ) = { 3   ,   4   ,   6 } endpos(\text{b}) = \{3\ , \ 4\ , \ 6\} endpos(b)={3 , 4 , 6} b \text{b} b 属于状态 5 5 5 ,在接下来 e n d p o s ( ∅ ) = { 0   ,   1   ,   2   ,   3   ,   4   ,   5   ,   6 } endpos(\empty) = \{0\ , \ 1\ , \ 2\ , \ 3\ , \ 4\ , \ 5\ , \ 6\} endpos()={0 , 1 , 2 , 3 , 4 , 5 , 6} ∅ \empty 属于状态 S S S .

顺着走下来可以发现一条状态路径 $7\rightarrow 8 \rightarrow 5 \rightarrow S $ ,这个序列的意义为 l ( 7 ) l(7) l(7) 的后缀依次出现在状态 7 7 7 8 8 8 5 5 5 S S S 中 .我们用后缀链接 SuffixLink \text{SuffixLink} SuffixLink 来把它们连接起来,对应上图的虚线.

转移函数

Next ( s ) \text{Next}(s) Next(s) 为以状态 s s s 开始下一个可能会遇到的字符集,即 Next ( st ) = { s [ i + 1 ]   ∣   i ∈ e n d p o s ( st ) } \text{Next}(\text{st}) = \{s[i+1]\ | \ i\in endpos(\text{st})\} Next(st)={s[i+1]  iendpos(st)} .

例子中 Next ( S ) = { s [ 0 ]   ,   …   ,   s [ 6 ] } = { a  ,  b  ,  d } \text{Next}(S)=\{s[0]\ , \ \ldots\ , \ s[6]\}=\{\text{a}\ , \ \text{b}\ , \ \text{d}\} Next(S)={s[0] ,  , s[6]}={a , b , d} .

Next ( 8 ) = { b  ,  d } \text{Next}(8) = \{\text{b}\ , \ \text{d}\} Next(8)={b , d} .

很显然, S ( st ) S(\text{st}) S(st) 后面接上一个 c ∈ Next ( st ) c\in \text{Next}(\text{st}) cNext(st) 会变成同一个状态,记这个状态为 trans ( st  ,   c ) \text{trans}(\text{st}\ ,\ c) trans(st , c) .

构造

我们使用 增量法 来进行构造 .

记录一下数据:

数据含义
m a x l e n [ s t ] maxlen[st] maxlen[st] s t st st 包含的最长子串长度
m i n l e n [ s t ] minlen[st] minlen[st] s t st st 包含的最短子串长度
t r a n s [ s t ] [ 1 … c ] trans[st][1\ldots c] trans[st][1c] s t st st 的转移函数,其中 1 … c 1\ldots c 1c 为字符集
l i n k [ s t ] link[st] link[st] s t st st 的后缀链接

考虑现在新加入的字符 s [ i + 1 ] s[i+1] s[i+1] ,设 s [ 1 … i ] s[1\ldots i] s[1i] 的状态为 u u u ,即 s [ 1 … i ] ∈ S ( u ) s[1\ldots i]\in S(u) s[1i]S(u) .

于是 s [ 2 … i ]   ,   s [ 3 … i ]   ,   …   ,   s [ i ]   ,   ∅ s[2\ldots i]\ , \ s[3\ldots i]\ , \ \ldots \ , \ s[i]\ , \ \empty s[2i] , s[3i] ,  , s[i] ,  的所属状态即为沿着 SuffixLink ( u ) \text{SuffixLink}(u) SuffixLink(u) 走的路径,记作 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) .

显然对于 s [ 1 … i ] = l ( u ) ∈ S ( u ) s[1\ldots i]= l(u)\in S(u) s[1i]=l(u)S(u) ,其任意后缀 t t t 要么在状态 u u u 里,要么在 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 上 .

由于 s [ 1 … i + 1 ] s[1\ldots i+1] s[1i+1] 不在任一已知状态中,因此开一个新的状态 z z z 来表示它 .

接下来考虑两种情况:

  1. 简单一点的,对于 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 中的任意状态 v v v ,都有 t r a n s [ v ] [ s [ i + 1 ] ] = ∅ trans[v][s[i+1]]=\empty trans[v][s[i+1]]= ,这时只需要令 t r a n s [ v ] [ s [ i + 1 ] ] = z trans[v][s[i+1]]=z trans[v][s[i+1]]=z ,并且 l i n k [ z ] = S link[z] = S link[z]=S 即可 .

14813690859339.png (568×175) (hihocoder.com)

在这个例子中, u = 2 u=2 u=2 z = 3 z=3 z=3 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 桔色状态 \color{orange}{\text{桔色状态}} 桔色状态 组成的路径 2 → 1 → S 2\rightarrow 1\rightarrow S 21S ,并且这三个状态都没有 b \text{b} b 的转移,因此只需添加 红色状态 \color{red}{\text{红色状态}} 红色状态 t r a n s [ 2 ] [ 1 ] = 3 trans[2][1] = 3 trans[2][1]=3 t r a n s [ 1 ] [ 1 ] = 3 trans[1][1] = 3 trans[1][1]=3 t r a n s [ S ] [ 1 ] = 3 trans[S][1] = 3 trans[S][1]=3 即可 ,当然还要 l i n k [ 3 ] = S link[3] = S link[3]=S .

  1. 难搞一点的为 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 中有一个状态 v v v ,使得 t r a n s [ v ] [ s [ i + 1 ] ] ≠ ∅ trans[v][s[i+1]]\not= \empty trans[v][s[i+1]]= .

14813699928939.png (757×629) (hihocoder.com)

上面的例子中已经构造出了 aabb \text{aabb} aabb SAM \text{SAM} SAM ,现在要添加一个字符 a \text{a} a 构造 aabba \text{aabba} aabba SAM \text{SAM} SAM .

这时 u = 4 u=4 u=4 z = 6 z=6 z=6 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 桔色状态 \color{orange}{\text{桔色状态}} 桔色状态 组成的路径 4 → 5 → S 4\rightarrow 5\rightarrow S 45S ,对于 4 4 4 5 5 5 都有 t r a n s [ 4 ] [ 0 ] = ∅ trans[4][0] = \empty trans[4][0]= t r a n s [ 5 ] [ 0 ] = ∅ trans[5][0]=\empty trans[5][0]= ,因此直接添加 红色转移 \color{red}{\text{红色转移}} 红色转移 t r a n s [ 4 ] [ 0 ] = t r a n s [ 5 ] [ 0 ] = 6 trans[4][0] = trans[5][0] = 6 trans[4][0]=trans[5][0]=6 即可,但此时 t r a n s [ S ] [ 0 ] = 1 trans[S][0]=1 trans[S][0]=1 已经存在了 .

不失一般性的,设 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 中遇到的第一个状态 v v v 满足 t r a n s [ v ] [ s [ i + 1 ] ] = x trans[v][s[i + 1]] = x trans[v][s[i+1]]=x ,接下来讨论 x x x 包含的的子串情况 .

  1. 如果 x x x 中包含的最长子串就是 v v v 中最长子串接上 s [ i + 1 ] s[i + 1] s[i+1] 后的子串,即 m a x l e n [ v ] + 1 = m a x l e n [ x ] maxlen[v] +1= maxlen[x] maxlen[v]+1=maxlen[x] ,这种情况比较简单,则只需增加 l i n k [ z ] = x link[z] = x link[z]=x 即可 .

例子中,只需让 l i n k [ 6 ] = 1 link[6]=1 link[6]=1 即可.

  1. 如果 x x x 中包含的最长子串不是 v v v 中最长子串接上 s [ i + 1 ] s[i+1] s[i+1] 后的子串,即 m a x l e n [ v ] + 1 < m a x l e n [ x ] maxlen[v]+1 < maxlen[x] maxlen[v]+1<maxlen[x] ,这种情况是重点讨论对象 .

14813690856741.png (841×719) (hihocoder.com)

Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 这条路径上,从 u u u 开始有一部分连续的状态满足 t r a n s [ st ] [ c ] = ∅ trans[\text{st}][\text{c}]=\empty trans[st][c]= ,则令 t r a n s [ st ] [ c ] = z trans[\text{st}][\text{c}]=z trans[st][c]=z 即可 .

紧接着有一部分连续的状态满足 t r a n s [ v … w ] [ c ] = x trans[v\ldots w][\text{c}]=x trans[vw][c]=x ,并且 l ( v ) + c ≠ l ( x ) l(v)+\text{c}\not= l(x) l(v)+c=l(x) .

这时我们就从 x x x 中拆出新的状态 y y y ,并且把所有原本属于 x x x 的满足长度小于 m a x l e n [ v ] + 1 maxlen[v]+1 maxlen[v]+1 的子串分给 y y y ,其余的子串留给 x x x ,同时令 t r a n s [ v … w ] [ c ] = y trans[v\ldots w][\text{c}] = y trans[vw][c]=y l i n k [ y ] = l i n k [ x ] link[y]=link[x] link[y]=link[x] l i n k [ x ] = l i n k [ z ] = y link[x] = link[z] = y link[x]=link[z]=y .

也就是 y y y 继承 x x x 的后缀链接,并且 x x x z z z 前面断开的 S S S 就存在于 y y y 中了 .

14813690864454.png (993×289) (hihocoder.com)

在这个例子中,先前已经够早了 aab \text{aab} aab SAM \text{SAM} SAM ,接下来要构造 aabb \text{aabb} aabb SAM \text{SAM} SAM .

u = 3 u=3 u=3 z = 4 z=4 z=4 ,处理 S S S 时遇到 t r a n s [ S ] [ 1 ] = 3 trans[S][1] = 3 trans[S][1]=3 l ( 3 ) = aab l(3)=\text{aab} l(3)=aab l ( S ) + b = b ≠ l ( 3 ) l(S)+\text{b}=\text{b}\not= l(3) l(S)+b=b=l(3) ,两者不相等 ,意味着 e n d p o s ( aab ) endpos(\text{aab}) endpos(aab) 已经不等于 e n d p o s ( b ) endpos(\text{b}) endpos(b) 了,势必这两个子串不能属于同一个状态 3 3 3 .

这时我们就需要从状态 3 3 3 中拆出一个状态 5 5 5 , 把 b \text{b} b 以及其后缀分给 5 5 5 , 其余的子串留给 3 3 3 . 同时令 t r a n s [ S ] [ 3 ] = 5 trans[S][3]=5 trans[S][3]=5 l i n k [ 5 ] = l i n k [ 3 ] = S link[5]=link[3]=S link[5]=link[3]=S l i n k [ 4 ] = l i n k [ 3 ] = 5 link[4]=link[3]=5 link[4]=link[3]=5 .

总时间复杂度 O ( ∣ S ∣ ) O(|S|) O(S) ,具体证明见 后缀自动机详解_DZYO的博客 .

由上述构造可知状态规模大致在 O ( 2 × n − 1 ) O(2\times n -1) O(2×n1) 范围, t r a n s trans trans 大致在 O ( 3 × n − 4 ) O(3\times n -4) O(3×n4) 规模 .

Code

struct Suffix_automata
{
	int maxlen[N] , minlen[N] , link[N] , trans[N][26];
	int Siz , Las;
	Suffix_automata() {Siz = Las = 1;}
	inline void Extend(int id)
	{
		int cur = ++Siz , p;
		maxlen[cur] = maxlen[Las] + 1;
		for(p = Las ; p && !trans[p][id] ; p = link[p]) trans[p][id] = cur;
		if(!p) link[cur] = 1;
		else
		{
			int q = trans[p][id];
			if(maxlen[q] == maxlen[p] + 1) link[cur] = q;
			else
			{
				int y = ++Siz;
				maxlen[y] = maxlen[p] + 1;
				memcpy(trans[y] , trans[q] , sizeof trans[q]);
				link[y] = link[q];
				for(; p && trans[p][id] == q ; p = link[p]) trans[p][id] = y;
				link[cur] = link[q] = y; 
			}
		}
		Las = cur;
		ans += maxlen[cur] - maxlen[link[cur]];
	}
} T;

应用

本质不同子串个数

实际上就是求所有 e n d p o s endpos endpos 的并,跳 Suffix-path \text{Suffix-path} Suffix-path 即可 .

任意子串出现次数

跳相当于查对应状态 e n d p o s endpos endpos 的大小,即 ∣ e n d p o s ( x ) ∣ |endpos(x)| endpos(x) .

对于状态 u u u ,沿着 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 走到的状态都是 l ( u ) l(u) l(u) 的后缀,因此 ∣ e n d p o s ( u ) ∣ |endpos(u)| endpos(u) 可以贡献给其路径上经过的每一个点,均摊下来等价于找 Suffix-path ( u → S ) \text{Suffix-path}(u\rightarrow S) Suffix-path(uS) 的起始端点 .

又由于 m a x l e n [ l i n k [ u ] ] < m i n l e n [ u ] maxlen[link[u]]<minlen[u] maxlen[link[u]]<minlen[u] ,因此沿着 l i n k link link 边走相当于走一个 DAG \text{DAG} DAG ,再由出边唯一,因此这是一棵树 .

起始端点就相当于这棵树的叶子,求一下即可 .

两个串的最长公共子串

还是增量构造 .

考虑将模式串的 SAM \text{SAM} SAM 建出来,设 T [ i ] T[i] T[i] 表示以模式串第 i i i 位为末尾的最长公共子串 .

以下面例子为例:

S:aabbabd
T:abbabb
1:a
2:ab
3:abb
4:abba
5:abbab
6:    ab

我们发现往后加一个字符相当于跳 t r a n s trans trans .

u , l u,l u,l 分别为当前 T [ i ] T[i] T[i] 所在的状态以及它在原串中的长度 .

假设已知上一个 T [ i − 1 ] T[i-1] T[i1] u ′ u' u l ′ l' l ,现在推 T [ i ] T[i] T[i] u u u l l l .

  1. t r a n s [ u ′ ] [ T [ i ] ] = v trans[u'][T[i]]=v trans[u][T[i]]=v v ≠ ∅ v\not=\empty v= ,那 u = v u=v u=v l = l ′ + 1 l=l'+1 l=l+1 即可.
  2. t r a n s [ u ′ ] [ T [ i ] ] = v trans[u'][T[i]]=v trans[u][T[i]]=v v = ∅ v=\empty v= ,那就跳 l i n k link link ,直到找到一个状态 u u uu uu 使得 t r a n s [ u u ] [ T [ i ] ] ≠ ∅ trans[uu][T[i]]\not= \empty trans[uu][T[i]]= ,注意到此时有 l ( u u ) + T [ i ] = S ( T [ i ] ) l(uu)+T[i]=S(T[i]) l(uu)+T[i]=S(T[i]) ,因此 u = t r a n s [ u u ] [ T [ i ] ] u=trans[uu][T[i]] u=trans[uu][T[i]] l = m a x l e n [ u u ] + 1 l=maxlen[uu]+1 l=maxlen[uu]+1 即可 . 不存在就直接 u = 0 u=0 u=0 l = 0 l=0 l=0 .

总时间复杂度 ∣ S ∣ + ∣ T ∣ |S|+|T| S+T ,因为最多把 l i n k link link t r a n s trans trans 全跳一遍 .

具体应用

P2178 [NOI2015] 品酒大会

一个朴素的想法是,将字符串倒置,然后建出 SAM \text{SAM} SAM .

考虑一个状态的 m i n l e n minlen minlen m a x l e n maxlen maxlen s i z siz siz ,这里的 s i z siz siz 表示 ∣ e n d p o s ∣ |endpos| endpos .

如果 s i z = 1 siz = 1 siz=1 ,那么很遗憾它的起点一样,说明拿不出两杯不同位置的酒(不过可以用来做 " 0 0 0 相似" 的情况).

否则这里边可以取出任意长度在 [ m i n l e n   ,   m a x l e n ] [minlen\ , \ maxlen] [minlen , maxlen] 之间的酒,并且他俩起始酒不属于同一杯酒 .

于是问题转化成便利状态,将 e n d p o s endpos endpos 外挂上状态上,选两个最大的乘起来再线段树区间赋值即可,最后这步离线,因此也可以打标记单调队列 .

但这样会有个小问题是,挂美味度时可能会被卡到 O ( n 2 ) O(n^2) O(n2) .

于是将美味值挂在叶子上,转移时继承即可 .

实际写的时候,还可以写成 DAG \text{DAG} DAG 式的转移 .

P4248 [AHOI2013]差异

考虑意义,等价于反串 p a r e n t parent parent 树上两个节点路径之和,其中每条边的权值为 m a x l e n [ u ] − m a x l e n [ l i n k [ u ] ] maxlen[u]-maxlen[link[u]] maxlen[u]maxlen[link[u]] .

拆掉,对单条边考虑贡献即可 .

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值