2022年01月20日,第七天
后缀自动机 是一个能解决许多字符串相关问题的有力数据结构。
以下的字符串问题都可以在线性的时间复杂度内通过 S A M SAM SAM 解决。
直观上,字符串的 S A M SAM SAM 可以理解为给定字符串的所有子串的压缩形式。
一些保证线性的性质:
字符串 s s s 的 S A M SAM SAM 是一个接受 s s s 的所有的最小 D F A DFA DFA (确定性有限自动机或确定性有限状态自动机)。
换句话说:
S A M SAM SAM 最简单、也最重要的性质是,它包含关于字符串 s s s 的所有子串的信息。任意从初始状态 t 0 t_0 t0 开始的路径,如果我们将转移路径上的标号写下来,都会形成 s s s 的一个 子串 。反之每个 s s s 的子串对应从 t 0 t_0 t0 开始的某条路径。
为了简化表达,我们称子串 对应 一条路径(从 t 0 t_0 t0 开始、由一些标号构成这个子串)。反过来,我们说任意一条路径 对应 它的标号构成的字符串。
到达某个状态的路径可能不止一条,因此我们说一个状态对应一些字符串的集合,这个集合的每个元素对应这些路径。
在描述线性时间内构造 S A M SAM SAM 的算法之前,我们需要引入几个对理解构造过程非常重要的概念并对其进行简单证明。
结束位置 e n d p o s endpos endpos
考虑字符串 s s s 的任意非空子串 t t t ,我们记 e n d p o s ( t ) endpos(t) endpos(t) 为在字符串 s s s 中 t t t 的所有结束位置 (假设对字符串中字符的编号从零开始)。例如,对于字符串 “ a b c b c ” “abcbc” “abcbc” ,我们有 e n d p o s ( “ b c ” ) = 2 , 4 endpos(“bc”)=2,\ 4 endpos(“bc”)=2, 4 。
两个子串 t 1 t_1 t1 与 t 2 t_2 t2 的 e n d p o s endpos endpos 集合可能相等: e n d p o s ( t 1 ) = e n d p o s ( t 2 ) endpos(t_1)=endpos(t_2) endpos(t1)=endpos(t2) 。这样所有字符串 s s s 的非空子串都可以根据它们的 e n d p o s endpos endpos 集合被分为若干 等价类 。
显然, S A M SAM SAM 中的每个状态对应一个或多个 e n d p o s endpos endpos 相同的子串。换句话说, S A M SAM SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。 S A M SAM SAM 的状态个数等价于 e n d p o s endpos endpos 相同的一个或多个子串所组成的集合的个数 + 1 +1 +1 。
由 e n d p o s endpos endpos 的值我们可以得到一些重要结论:
引理1:两个非空子串 u u u 和 w w w (假设 ∣ u ∣ ≤ ∣ w ∣ |u|\le|w| ∣u∣≤∣w∣) 的 e n d p o s endpos endpos 相同,当且仅当字符串 u u u 是 w w w 的后缀。
引理2:考虑两个非空子串 u u u 和 w w w (假设 ∣ u ∣ ≤ ∣ w ∣ |u|\le|w| ∣u∣≤∣w∣)。那么要么 e n d p o s ( u ) ∩ e n d p o s ( w ) = ∅ endpos(u)\cap endpos(w)=\varnothing endpos(u)∩endpos(w)=∅ ,要么 e n d p o s ( w ) ⊆ e n d p o s ( u ) endpos(w)\subseteq endpos(u) endpos(w)⊆endpos(u) ,取决于 u u u 是否为 w w w 的一个后缀:
{ e n d p o s ( w ) ⊆ e n d p o s ( u ) i f u i s a s u f f i x o f w e n d p o s ( w ) ∩ e n d p o s ( u ) = ∅ o t h e r w i s e \begin{cases} \begin{aligned} &endpos(w)\subseteq endpos(u) & if\ u\ is\ a\ suffix\ of\ w\\ &endpos(w)\cap endpos(u)=\varnothing & otherwise \end{aligned} \end{cases} {endpos(w)⊆endpos(u)endpos(w)∩endpos(u)=∅if u is a suffix of wotherwise
引理3:考虑一个 e n d p o s endpos endpos 等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,于此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任意两个子串,较短者为较长者的后缀,且该等价类中的字串长度恰好覆盖整个区间 [ x , y ] [x,y] [x,y] 。
后缀链接 l i n k link link
考虑 S A M SAM SAM 中某个不是 t 0 t_0 t0 的状态 v v v 。我们已经知道,状态 v v v 对应于具有相同 e n d p o s endpos endpos 的等价类。我们如果定义 w w w 为这些字符串中最长的一个,则所有其它的字符串都是 w w w 的后缀。
我们还知道字符串 w w w 的前几个后缀(按长度降序考虑)全部包含于这个等价类,且所有其它后缀(至少有一个——空后缀)在其它的等价类中。我们记 t t t 为最长的这样的后缀,然后将 v v v 的后缀链接连到 t t t 上。
换句话说,一个 后缀链接 l i n k ( v ) link(v) link(v) 链接到对应于 w w w 的最长后缀的另一个 e n d p o s endpos endpos 等价类的状态。
以下我们假定初始状态 t 0 t_0 t0 对应于它自己这个等价类(只包含一个空字符串)。为了方便,我们规定 e n d p o s ( t 0 ) = { − 1 , 0 , ⋯ , ∣ S ∣ − 1 } endpos(t_0)=\{-1,0,\cdots,|S|-1\} endpos(t0)={−1,0,⋯,∣S∣−1} 。
引理4:所有后缀链接构成一棵根结点为 t 0 t_0 t0 的树。
引理5:通过 e n d p o s endpos endpos 集合构造的树(每个子节点的 s u b s e t subset subset 都包含在父节点的 s u b s e t subset subset 中)与通过后缀链接 l i n k link link 构造的数相同。
小结:在学习算法本身前,我们总结一下之前学过的知识,并引入一些辅助记号。
s s s 的子串可以根据它们结束的位置 e n d p o s endpos endpos 被划分为多个等价类。
S A M SAM SAM 由初始状态 t 0 t_0 t0 和与每一个 e n d p o s endpos endpos 等价类对应的每个状态组成。
对于每一个状态 v v v ,一个或多个子串与之匹配。我们记 l o n g e s t ( v ) longest(v) longest(v) 为其中最长的一个字符串,记 m a x l e n g t h ( v ) maxlength(v) maxlength(v) 为它的长度。类似地,记 s h o r t e s t ( v ) shortest(v) shortest(v) 为最短的子串,它的长度为 m i n l e n g t h ( v ) minlength(v) minlength(v) 。那么对应这个状态的所有字符串都是字符串 l o n g e s t ( v ) longest(v) longest(v) 的不同的后缀,且所有字符串的长度恰好覆盖区间 [ m i n l e n g t h ( v ) , m a x l e n g t h ( v ) ] [minlength(v),\ maxlength(v)] [minlength(v), maxlength(v)] 中的每一个整数。
对于任意不是 t 0 t_0 t0 的状态 v v v ,定义后缀链接为连接到对应字符串 l o n g e s t ( v ) longest(v) longest(v) 的长度为 m i n l e n g t h ( v ) − 1 minlength(v)-1 minlength(v)−1 的后缀的一条边。从根节点 t 0 t_0 t0 出发的后缀链接可以形成一棵树。这棵树也表示 e n d p o s endpos endpos 集合间的包含关系。
对于 t 0 t_0 t0 以外的状态 v v v ,可用后缀链接 l i n k ( v ) link(v) link(v) 表达 m i n l e n g t h ( v ) minlength(v) minlength(v) :
m i n l e n g t h ( v ) = m a x l e n g t h ( l i n k ( v ) ) + 1 minlength(v) = maxlength(link(v)) + 1 minlength(v)=maxlength(link(v))+1
如果我们从任意状态 v 0 v_0 v0 开始顺着后缀链接遍历,总会到达初始状态 t 0 t_0 t0 。这种情况下我们可以得到一个互不相交的区间 [ m i n l e n g t h ( v i ) , m a x l e n g t h ( v i ) ] [minlength(v_i), maxlength(v_i)] [minlength(vi),maxlength(vi)] 的序列,且它们的并集形成了连续的区间 [ 0 , m a x l e n g t h ( v 0 ) ] [0, maxlength(v_0)] [0,maxlength(v0)] 。
S A M SAM SAM 有 O ( ∣ S ∣ ) O(|S|) O(∣S∣) 的构造方法,为了实现 O ( ∣ S ∣ ) O(|S|) O(∣S∣) 的构造,对于每个状态肯定不能保存太多数据,例如 s u b s t r ( s t a t e ) substr(state) substr(state) 肯定无法保存下来。
对于状态 s t a t e state state 我们只保存如下数据:
在该方法中,使用 增量法 构造 S A M SAM SAM ,从初始状态开始,每次考虑添加一个字符 S [ 1 ] , S [ 2 ] , ⋯ , S [ N ] S[1],S[2],\cdots,S[N] S[1],S[2],⋯,S[N],依次构造可以识别 S [ 1 ] , S [ 1 ⋯ 2 ] , S [ 1 ⋯ 3 ] , ⋯ , S [ 1 ⋯ N ] S[1],S[1\cdots2],S[1\cdots3],\cdots,S[1\cdots N] S[1],S[1⋯2],S[1⋯3],⋯,S[1⋯N] 的 S A M SAM SAM 。
假设已经构造好了 S [ 1 ⋯ i ] S[1\cdots i] S[1⋯i] 的 S A M SAM SAM ,要添加字符 S [ i + 1 ] S[i + 1] S[i+1] ,相当于新增了 i + 1 i + 1 i+1 个 S [ i + 1 ] S[i+1] S[i+1] 的后缀要识别,分别为: S [ 1 ⋯ i + 1 ] , S [ 2 ⋯ i + 1 ] , ⋯ , S [ i ⋯ i + 1 ] , S [ i + 1 ] S[1\cdots i + 1],S[2\cdots i + 1], \cdots,S[i\cdots i + 1], S[i+ 1] S[1⋯i+1],S[2⋯i+1],⋯,S[i⋯i+1],S[i+1] 。
这些新增状态分别是从 S [ 1 ⋯ i ] , S [ 2 ⋯ i ] , S [ 3 ⋯ i ] , S [ i ] , _ S[1\cdots i],S[2\cdots i],S[3\cdots i],S[i],\_ S[1⋯i],S[2⋯i],S[3⋯i],S[i],_(空串) 通过字符 S [ i + 1 ] S[i+1] S[i+1] 转移过来的。
假设 S [ 1 ⋯ i ] S[1\cdots i] S[1⋯i] 所在的状态是 u u u ,即: S [ 1 ⋯ i ] ∈ s u b s t r ( u ) S[1\cdots i]\in substr(u) S[1⋯i]∈substr(u) , S [ 1 ⋯ i ] , S [ 2 ⋯ i ] , S [ 3 ⋯ i ] , ⋯ , S [ i ] , _ S[1\cdots i],S[2\cdots i],S[3\cdots i],\cdots,S[i],\_ S[1⋯i],S[2⋯i],S[3⋯i],⋯,S[i],_(空串) 对应的状态恰好是从 u u u 到初始状态 S S S 的由 后缀链接 连接起来路径上的所有状态,不妨称这条路径上所有状态集合是 s u f f i x _ p a t h ( u → S ) suffix\_path(u\rightarrow S) suffix_path(u→S) ,显然至少 S [ 1 ⋯ i + 1 ] S[1\cdots i + 1] S[1⋯i+1] 不能被以前的 S A M SAM SAM 识别,所以我们至少需要添加一个状态 z z z , z z z 至少包含 S [ 1 ⋯ i + 1 ] S[1\cdots i + 1] S[1⋯i+1] 。
情况一 :对于 s u f f i x _ p a t h ( u → S ) suffix\_path(u\rightarrow S) suffix_path(u→S) 的任意状态 v v v ,都有 c h [ v ] [ S [ i + 1 ] ] = N U L L ch[v][S[i+1]]=NULL ch[v][S[i+1]]=NULL 。
这时我们只要令 c h [ v ] [ S [ i + 1 ] ] = z ch[v][S[i+1]]=z ch[v][S[i+1]]=z ,并且令 l i n k [ z ] = S link[z]=S link[z]=S (这里的 S S S 表示源点)即可。
情况二: s u f f i x _ p a t h ( u → S ) suffix\_path(u\rightarrow S) suffix_path(u→S) 上存在结点 v v v ,满足 c h [ v ] [ S [ i + 1 ] ] ≠ N U L L ch[v][S[i+1]]\not= NULL ch[v][S[i+1]]=NULL 。
我们可以认为在 s u f f i x _ p a t h ( u → S ) suffix\_path(u\rightarrow S) suffix_path(u→S) 遇到的第一个状态 v v v 满足 c h [ v ] [ S [ i + 1 ] ] = x ch[v][S[i+1]]=x ch[v][S[i+1]]=x 。这时讨论 x x x 包含的子串的情况,分为 A , B A,B A,B 两类:
A A A 类: 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) , z z z 的后缀链接恰好是 x x x 。若在 s u f f i x _ p a t h ( u → S ) suffix\_path(u\rightarrow S) suffix_path(u→S) 上 v v v 从 p r e _ v pre\_v pre_v 转移,则 m a x l e n ( x ) = m a x l e n ( v ) + 1 = m i n l e n ( p r e _ v ) = m i n l e n ( z ) − 1 maxlen(x)=maxlen(v)+1=minlen(pre\_v)=minlen(z)-1 maxlen(x)=maxlen(v)+1=minlen(pre_v)=minlen(z)−1 。
此时只要增加 l i n k [ z ] = x link[z]=x link[z]=x 即可。既然 x x x 存在,那么 x x x 的后缀一定也存在,本次增量完成。
B B B 类: 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) 。这种情况下增加的字符是 c c c ,状态是 z z z 。在 s u f f i x _ p a t h ( u → S ) suffix\_path(u\rightarrow S) suffix_path(u→S) 这条路径上,从 u u u 开始有一部分连续的状态满足 c h [ u ⋯ ] [ c ] = z ch[u\cdots][c]=z ch[u⋯][c]=z ,紧接着有一部分连续的状态 v ⋯ w v\cdots w v⋯w 满足 c h [ v ⋯ w ] [ c ] = x ch[v\cdots w][c]=x ch[v⋯w][c]=x ,并且 m a x s u b ( v ) + c maxsub(v)+c maxsub(v)+c 不等于 m a x s u b ( x ) maxsub(x) maxsub(x) 。
此时只要从 x x x 拆分出新的状态 y y y ,并且把原来 x x x 中长度小于等于 m a x s u b ( v ) + c maxsub(v) + c maxsub(v)+c 的子串分给 y y y ,其余子串留给 x x x 。同时令 c h [ v ⋯ w ] [ c ] = y , l i n k [ y ] = l i n k [ x ] , l i n k [ x ] = l i n k [ z ] = y ch[v\cdots w][c]=y,link[y]=link[x],link[x]=link[z]=y ch[v⋯w][c]=y,link[y]=link[x],link[x]=link[z]=y 。也就是 y y y 先继承 x x x 的 l i n k link link ,并且 x , z x,z x,z 前面断开的 s u b s t r i n g s substrings substrings 就存在于 y y y 中了。
由此产生了以下短小精悍的代码QwQ:
const int N = 1e6 + 5;
struct node {
int len, link, ch[26];
}st[N << 1];
int sz, last;
void sam_init () { sz = last = 1; } // 为方便操作,这里我们从 1 开始
void sam_extend (int c) {
int cur = ++ sz, p = last;
last = cur, st[cur].len = st[p].len + 1;
for ( ; p && ! st[p].ch[c]; p = st[p].link) st[p].ch[c] = cur;
if (p == 0) st[cur].link = 1; // 情况一
else {
int q = st[p].ch[c];
if (st[p].len + 1 == st[q].len) st[cur].link = q; // 情况二 A类
else { // 情况二 B类
int clone = ++ sz;
st[clone].len = st[p].len + 1;
st[clone].link = st[q].link;
memcpy (st[clone].ch, st[q].ch, sizeof (int)*26);
for ( ; p && st[p].ch[c] == q; p = st[p].link) st[p].ch[c] = clone;
st[q].link = st[cur].link = clone;
}
}
}