[后缀自动机]SAM的一点理解

(啊,图片好像还有CSDN水印呢)

主要参考资料:CLJppt。

预备知识

自动机组成:状态、初始状态、终止状态、状态转移、字符集。

什么是状态?

经典图片:

ACADD对应的SAM

对于整个串而言,初始状态(以下简称为init)为ROOT,终止状态集合(以下简称end)为最上方及最右方的那两个写着D的(状态既不是字符,也不是子串,在这里把它理解为某个下标更好),所有的状态就是那七个圈,每条实线边代表从一个状态向另一个状态的状态转移。字符集不太重要,在这里为26个字母(暂不区分大小写)。

定义的标注

为便于阅读及写作,下文中,定义均使用

  • 定义

的格式进行标注。当发现有问题时,不妨回到开头再看一眼定义

正文

Part 1 自动机&&后缀自动机的一些定义

  • \(trans()\)代表状态转移函数(即上文所说边)。
  • \(trans(s,ch)\)表示从状态s起,通过字符ch转移所到达的状态(就是走标号为ch的边到达的点)。
  • \(trans(s,ch)\)表示的状态若不存在,则值为NULL。同时规定\(trans(NULL,any\underline{\;}possible\underline{\;}value)\)只能为NULL。

例如,上图中,如果当前状态在左下角的A这个,通过'D'可以转移到右下角倒数第二个D这个,通过'C'可以转移到。。。唯一的C所在的

由于一直用**圈**表述状态实在太麻烦,因此在不引起歧义的前提下,我也会使用某个字符来表示状态。但请时刻注意,状态并非字符。

  • \(trans(s,str)\)表示从s状态起,通过字符串str转移后到达的状态。

例如,上图中,从init通过字符串"AC"转移可以到达唯一的那个C。

很明显,有如下函数(伪代码):

trans(s,str)
    cur = s;
    for i = 0 to len(str) - 1
        cur = trans(cur, str[i])
    return cur

就是说,\(trans(s,str)\)可以看成是一大堆\(trans(s,ch)\)的总和。

  • 于是某个自动机A能识别的字符串就是所有使得\(trans(init,x) \in end\)的字符串x。记为\(Reg(A)\)

(说白了就是把x放到ROOT上开始沿着边跑,如果在end中节点上结束,就说能识别。栗子:我往一颗Trie树中插了字符串"ACADD",那这颗Trie就能识别"ACADD",不能识别"ACA"、"BC"等等奇奇怪怪的字符串)

  • 从状态s开始能识别的字符串,就是所有使得\(trans(init,x) \in end\)的字符串,记为\(Reg(s)\)
  • \(Reg()\)的值是个集合。

比如,还是刚才那图,从C开始,"DD"可以被识别(因为\(trans(C\)所在的\("DD") \in end\)

  • S的后缀自动机(以下简称为SAM),能够识别S的所有后缀。

也就是说,当且仅当x是S的后缀时,\(x \in Reg(SAM)\)。既然能识别所有的后缀,那我们扩展end集合后,SAM也能识别S所有后缀的前缀,即S的子串。

Part 2 减少状态及一些性质

当然我们可以把一个串的所有后缀都插入Trie树,这当然也是后缀自动机,但是时间、空间复杂度都是\(O(n^2)\)

考虑到这样一颗Trie树会有很多重复的字符,因此我们需要最简状态后缀自动机,即状态数(就是需要的圈)最少的后缀自动机。

一些性质

  • \(ST(str)=trans(init,str)\)

定义\(ST(str)\)为初始状态读入str(以下用“读入%s”表示“通过%s转移”)后到达的状态。

  • \(Suf\):原串S后缀的集合
  • \(Fac\):原串S子串的集合
  • \(S[l,r)\)表示S中\([l,r)\)这个区间构成的子串。
  • \(Suffix(a)\)表示从位置a开始的后缀,即\(S[a,len(s))\)
  • 约定S下标从0开始。
  • \(l>=r\)\(S[l,r)=""\)

对于一个字符串s,如果\(s \notin Fac\),那么\(ST(s)=NULL\)(显然)。而如果\(s \in Fac\),那么\(ST(S) \not= NULL\),因为如果s是S的子串,在s后面加上一些字符就可能变为S的后缀。

考虑朴素的插入,是对每个\(s \in Fac\)新建一个状态(既然对所有后缀建一条链,那对于某个后缀的前缀的最后一个字符,都要新建一个状态,那么可以看作对每个子串新建了一个状态),这样做是\(O(n^2)\)的。

考虑对于某个字符串\(a\)\(ST(a)\)能识别那些字符串,即从\(ST(a)\)起通过那些字符串可转移到end,即\(Reg(ST(a))\)

\(x \in Reg(SAM)\),当且仅当\(x \in Suf\)

\(x \in Reg(ST(a))\),当且仅当\(ax \in Suf\)

也就是说ax是S后缀,那么x必定也是S后缀。\(\therefore Reg(ST(a))\)是一些后缀集合。

现在,对于一个状态s,我们只关注\(Reg(s)\)

对于某个子串a,如果\(S[l,r)==a\),那么\(ST(a)\)能识别\(Suffix(r)\)

如果a在S中出现的位置的集合为\(\{(l,r)|S[l,r)==a\}=\{[l_1,r_1),[l_2,r_2),...,[l_n,r_n)\}\),那么\(Reg(ST(a))=\{Suffix(r_1),Suffix(r_2),...,Suffix(r_n)\}\)

不妨令\(Right(a)=\{r_1,r_2,...,r_n\}\)

那么\(Reg(ST(a))\)完全由\(Right(a)\)决定。

又因为我们要使状态数最少,那所有状态s应该与所有\(Reg(s)\)一一对应(首先,每个状态s一定能映射到一个Reg集合。然后,如果两个s映射到同一个Reg集合,为什么不把它们合并呢?)。也就是说,\(Reg(ST(a))==Reg(ST(b)) \Leftrightarrow ST(a)==ST(b)\)

那么对于两个子串\(a,b \in Fac\),如果\(Right(a)=Right(b)\),那么\(Reg(ST(a))=Reg(ST(b))\),即\(ST(a)=ST(b)\)

  • \(Right(ST(a))=Right(a)\)

所以一个状态s,可以由所有\(Right\)集合是\(Right(s)\)的字符串从init转移到达。记这些字符串为\(Sub(s)\)

  • \(Sub(s)=\{str|ST(str)=s\}\)

不妨设\(r \in Right(s)\),那么如果再给出一个合法的子串长度len,那么这个子串就是\(S[r-len,r)\)。即给定某个合法的(在这里指存在某个状态s的Right集合等于它)Right集合后,再给出一个合法的长度就可以确定子串了。

考虑对于一个Right集合,如果对于串长为\(len=l\ or\ len=r\)的子串是合法的,那么对于\(len \in [l,r]\)的子串也一定合法。

一些说明:设Right集合\(R_1\)中有一元素 \(substring\underline{\;}r_{1}\) ,在 \(len=l\ or\ len=r(l<=r)\) 的情况下 \(Right(S[substring\underline{\;}r_{1}-len,substring\underline{\;}r_{1}))=R_1\) ,那么情况如图。

很显然,对于\(len \in [l,r],S[substring\underline{\;}r_1-len,substring\underline{\;}r_1)\),都可以且仅可以识别R1中元素开始的后缀。而对于\(len>r\),只能保证仅可以;对于\(len<l\),只能保证可以。因此在\(len \notin [l,r]\)情况下,不保证\(Right(S[substring\underline{\;}r_1-len,substring\underline{\;}r_1))=R_1\)

所以对于一个Right集,合法的长度只能存在于一个区间内。

不妨设对于一状态s,合法的区间长度为\([Min(s),Max(s)]\)

状态数线性证明

考虑两个状态a,b,各自的Right集合分别为\(R_a,R_b\)

假设\(R_a \cap R_b \not= \emptyset\),设\(r \in R_a \cap R_b\)

由于\(Sub(a),Sub(b)\)不会有交集(\(trans(init,str)\)只能到达一个状态),所以\([Min(a),Max(a)]\)\([Min(b),Max(b)]\)也不可能有交集(不然的话以r为结束的某些子串会既到达a,又到达b)。

不妨设\(Max(a)<Min(b)\),那么\(Sub(a)\)中所有串长度都小于\(Sub(b)\)中的串。由于\(Sub(a),Sub(b)\)中的串都可以接上\(Suffix(r)\)\(Right\)定义),那么\(Sub(a)\)中串必定是\(Sub(b)\)中串的后缀。

于是\(Sub(b)\)中某串出现的位置上,\(Sub(a)\)中某串也必然能在这里出现。因此\(R_b \subset R_a\)。又\(R_a \not= R_b\),所以\(R_b \subsetneqq R_a\)

于是,\(Fac\)中任意两串的\(Right\)集合,要么不相交,要么一个是另一个的真子集,要么完全相同。

那么,如果我们把所有的\(Right\)集合组成集合,我们可以把他们组织成一个树的结构(如上图)。我们把它叫做Parent树。

很明显,这棵树叶节点有n个,而每个非叶子节点至少有两个孩子(不存在只有一个孩子),易证树的大小为\(O(n)\)级别(显然??)。

后面会用到的一些东西

令一个状态s,令\(fa=Parent(s)\)代表唯一的那个\(Right\)集合\(Right(s)\)父节点对应的集合的状态(这句话有点绕口,多读)。那么自然有\(Right(s) \subsetneqq Right(fa)\),并且\(Right(fa)\)的大小是其中最小的。

考虑\(len(Sub(s))\)\(len(Sub(fa))\)。则\(len(Sub(s)) \in [Min(s),Max(s)]\)。考虑\(Min(s)-1\)为什么不可以,因为长度变短,限制条件变少,于是可出现的位置多了。假设\(Right(s)=R_1\),还是这幅图:

这是\(Min(s)-1\)的情况:

显然,这个变大后的\(Right\)集是所有包含\(R_1\)的集合中最小的一个。因此它就是\(Right(fa)\)因此,\(Max(fa)=Min(s)-1\)(L就是\(Min(s)\)

状态转移数线性的证明

我们已经证明了状态数是\(O(n)\)的,为证明这是一个线性结构,还需证明它状态转移数也是\(O(n)\)的。

  • \(trans(s,a)=t\),则s向t连一条标号为a的边(就是s->ch[a] = t)。

自己看CLJ课件去!

显然法:状态是\(O(n)\)的,每个状态最多连出26条边,状态很显然是\(O(n)\)的。

Part 3 构造

回顾定义与性质

  • 状态s,转移trans,初始状态init,终止状态集合end。
  • 母串S,S的后缀自动机SAM。
  • \(Right(str)\)表示\(str\)在母串中所有出现的位置右端点集合。
  • \(Sub(s)\)中所有子串\(Right\)集合相同,为\(Right(s)\)
  • \(Parent(s)\)\(Parent\)树。

对于一个状态s,它的\(Right(s)=\{r_1,r_2,...,r_n\}\),现有s->ch[a]=t,考虑t的\(Right\)集合,由于t比s多一个字符,因此它限制更多,\(Right(t)=\{r_i+1|S[r_i]==c\}\)

终于可以把这张大图下放了。

例如,对这个"ACADD"的SAM,若s = root->ch['a']t = s->ch['d'],那么\(Right(s)=\{1,3\}\)(对应的后缀为"DD","ACDD"),\(Right(t)=\{4\}\)(对应的后缀为"D"),显然符合前面所说规则。(记住下标从0开始)

同时,如果s出发有标号为x的边,那么\(Parent(s)\)出发也一定有(因为\(s \subsetneqq Parent(s)\))。

\(f=Parent(s)\)。那么\(Right(trans(s,c)) \subset Right(trans(f,c))\)(因为\(s \subsetneqq f\))。

一个显然的推论是\(Max(t)>Max(s)\)。(对t而言,如果不考虑\(Min(t)\),那么\(len=Max(s)+1\)显然合适)

具体操作

  • \(SAM(str)\):str的后缀自动机

我们使用在线方法构造,即每次添加一个字符,使得当前SAM变成包含这个新字符的SAM(即,先构造\(SAM(S[0,i)\),再构造\(SAM(S[0,i+1)\))。

令当前字符串为T,新字符为x,令\(len(t)=L\)

\(SAM(T) \rightarrow SAM(Tx)\)的过程中,这个SAM可以多识别一些原串S的子串,这些子串都是Tx的后缀。

Tx的后缀,就是在T的后缀后面接上字符x。

考虑所有可以表示T后缀(即,\(Right\)集中包含L)的节点\(v_1,v_2,v_3,...\)

由于必然存在一个\(Right(p)=L\)的状态p(\(p=ST(T)\)),那么由于\(v_1,v_2,v_3...\)\(Right\)集合中都含有L,那么它们在\(Parent\)树中必然全是p的祖先(由于\(Parent树的性质\))。那么可以使用\(Parent\)函数得到它们。

同时我们添加一个字符x后,令\(np=ST(Tx)\),则此时\(Right(np)=L+1\)

不妨设\(v_1=p,v_2=Parent(v_1),v_3=Parent(v_2),...,v_k=Parent(v_{k-1})=root\)\(Right(root)=[0,L]\))。

考虑其中一个v的\(Right\)集合\(\{r_1,r_2,...,r_n\}(r_n=L)\),那么如果v->ch[x]=nv,则\(Right(nv)=\{r_i+1\ |\ S[r_i]==x\}\)。同时,如果v->ch[x]=NULL,那么v的\(Right\)集合内就没有一个\(r_i\)使得\(S[r_i]==x\)(先不考虑\(v_n\))。

又由于\(Right(v_1) \subsetneqq Right(v_2) \subsetneqq Right(v_3)...\),那么如果v[i]->ch[x]!=NULL,那么v[j]->ch[x]也一定不是NULL(\(j>i\))。

情况1

对于v->ch[x]==NULL的v,它的\(Right\)集合内只有\(r_n\)满足要求,所以根据之前提到的规则,使得v->ch[x]=np

情况2

\(v_p\)\(v_1,v_2,v_3,...\)中第一个ch[x]!=NULL的状态。

考虑\(Right(v_p)=\{r_1,r_2,...,r_n\}\),令\(trans(v_p,x)=q\)

那么\(Right(q)=\{r_i+1|S[r_i]==x\}\)(不考虑 \(r_n\) )。

但是,我们不一定能直接在\(Right(q)\)中插入\(L+1\)(暂且不管如何插入)。

如果在\(Right(q)\)中直接插入\(L+1\),由于限制条件增加,可能使\(Max(q)\)变小。

放图:

红色是结束在\(Right(v_p)\)位置上,长度为\(Max(v_p)\)的串,蓝色为结束在\(Right(q)\)位置上,长度为\(Max(q)\)的串。

于是强行做的话,\(Max(q)\)变小就很显然了。

当然,如果\(Max(q)==Max(v_p)+1\),就不会有这样的问题(这个很显然,把上图蓝色串开头的A改成B就行了),直接插入即可。

怎么插入?\(Parent(np)=q\)

证明:很显然\(Right(np) \subsetneqq Right(q)\)(q不会是\(ST(T)\)\(L+1 \in Right(q)\)),所以只需证明\(Right(q)\)是所有包含\(Right(np)\)的集合中大小最小的。反证法,假设存在一个包含\(Right(np)\)的集合\(Right(f)\),且\(Parent(f)=q\),那么\(Min(f)=Max(q)+1\),于是当\(len=Max(q)+1\)时,\(len \in [Min(f),Max(f)]\)。设\(Right(q)=\{r_1,r_2,...,r_k,...,r_n\}\)\(Right(f)=\{r_1,r_2,...,r_k,r_n\}\),看图。

如果真是这样,\(Right(p)\)不会是图上的\(Right(p)\),其中上方左数第一条边不应该存在(左数第三条可能也不存在)。因为只需要第2、4条边就可以使p->ch[x]!=NULL了,而这时第一条边显然还没有(加入它后Right(p)会变小)。于是产生矛盾,由此命题得证。

情况3

在2的情况下再减去一些限制条件又会怎样呢?

比如,现在没有了\(Max(q)==Max(v_p)+1\)

这个条件这么好,没有了岂不是可惜?

没有条件,创造条件。

上图中,红色为\(Right(v_p)\),蓝色为\(Right(q)\),绿色为\(Right(nq)\)

我们把q拆成两份:新建一个节点nq,使\(Right(nq)=Right(q) \cup \{L+1\}\)。这时,我们可以证明\(Max(nq)=Max(v_p)+1\),由于证法与上面那个证明非常类似所以不说了。

于是\(Right(q),Right(np) \subsetneqq Right(nq)\),且\(Right(np)+Right(q)=Right(nq)\)(图上可以看出)。于是,\(Parent(np)=Parent(q)=Parent(nq)\)

又由于\(Min(q)=Min(nq)\)(感性认知),所以\(Parent(nq)=Parent(q)\)(原来的)。

然后,\(trans(nq,x)=trans(q,x)\)(因为多出来的那个点再转移过程中没有用)。

接着,我们回头考虑\(v_1,v_2,...,v_k\)这一序列。其中最早在\(v_p\)ch[x]!=NULL。于是乎只有一段\(v_p,...,v_e\)(不是\(v_p,...,v_k\),因为\(v_e\)之后\(Right(v->ch[x])\)会更大)通过x边转移到q。那么现在把这一段的\(trans(\ast,x)\)改为nq既可。

结束了!

//源文件竟有4000字了?

posted @ 2019-10-15 18:13  artart  阅读(411)  评论(0编辑  收藏  举报