后缀自动机
有穷自动机
有穷自动机(FA)可以表示成如下的形式
其中
s0
表示初始状态,
S
表示状态集合,
这样,任意读入一个字符串,我们可以通过指定一台有穷自动机来判断读入的字符串是否符合我们的预期
后缀自动机
简述
后缀自动机是一台有穷自动机,用以识别一个字符串是否是给定字符串 a 的后缀,其具有以下的特点:
- 自动机中从初始状态到其它非初始状态的一条路径,构成
a 的一个子串- 对于
a
的每一个子串
s⊂a ,用 right(s) 表示字串 s 在原串a 中的结束位置。例如 a=ababa , s=aba ,那么 right(s)={3,5} - 对于两个不相同的子串
x
和
y ,如果 right(x)=right(y) ,那么我们可以认为在后缀自动机中, x 和y 是等价的,因为从两个子串开始,会出现相同的原串后缀。也就是说,在自动机中我们可以用一个状态 p 来表示这两个子串,那么定义状态的right 集合 right_S(p)={right(a)|a是s0到p的一条路径} - 对于两个
right
集合不相同的子串
x
和
y ,假设 r=right(x)∩right(y) ,那么可以看出, x 和y 两个子串同时在 r 处结束,那么这两个子串一定有一个是另一个的后缀。不妨令x 是 y 的后缀,那么一定有right(y)⊂right(x) ,因此,两个 right 集合不相同的子串,要么一个是另一个的真子集,要么就完全不相交。 - 现在回过头来考虑第二条特点,两个不相同的子串,
right
集合完全相同,一定也存在后缀性质。因此可以得知,任意一个状态
p
,所能够代表的子串,长度一定存在一个区间当中,假设是
[Min(p),Max(p)] 。那么对于另一个状态 q ,如果right_S(q)⊃right_S(p) ,且 Max(q)=Min(p)−1 ,那么我们可以定义 q 是p 的父亲状态,那么这种父亲状态的关系可以构成一棵树,我们称之为 parent 树,状态 q 所代表的子串称作p 所代表的子串的真·后缀 构造
后缀自动机的构造,我们使用一种叫做增量法的方式,依次向自动机中插入一个字符,直到字符串中字符全部插入自动机中。
假设我们现在添加到第 i 个字符a[i] ,令 last 表示添加第 i−1 个字符之后所处的状态,令 np 表示添加字符 a[i] 新建的状态。首先,很显然,任意以 a[i−1] 为结尾的子串,再加上一个字符 a[i] ,也是原串的子串,那么无条件地,所有以 a[i−1] 为结尾的子串,都要向状态 np 有一条状态转移的边;换句话说,串 a[1..i−1] 的所有后缀,都要向后再延伸一个字符 a[i] 。
如果某一个状态 p ,它已经有一条向后延伸字符a[i] 的边,假设这条边转移的状态是 q 。那么对于q 来讲,现在它的 right 集合,要多一个结束位置为 i ,并且这个子串的长度为Max(p)+1 。由于 q 是p 经过一条边转移得到的,那么很显然有 Max(q)≥Max(p)+1 。现在考虑:- 如果
Max(q)=Max(p)+1
,那么
q
状态现在代表的字符串没有任何需要变动,只是需要在
right 集合中添加一个元素而已,我们并不用改动。 - 如果
Max(q)>Max(p)+1
,那么
q
状态可以被分成两部分,一部分是长度为
Max(p)+1 的字符串,这部分字符串的 right 集合多了一个位置 i ,另一部分并不用改变。由于现在状态q 所代表的字符串的 right 集合出现了分歧,现在考虑将状态 q 分成两部分,分别代表上述的两部分字符串,依然用q 代表 [Max(p)+2,Max(q)] 部分,用新状态 nq 代表最长长度是 Max(p)+1 的部分,可以发现, nq 所代表的字符串一定是 q 代表的字符串的真后缀,并且是最近的真后缀,那么q 的父亲就是 nq
现在我们可以发现,其实状态 last ,代表的就是串 a[1..i−1] (最长的情况下),那么找寻串 a[1..i−1] 的所有后缀时,其实可以分为两部分,一部分在状态 last 里面包含着,另一部分就是状态 last 的真后缀,我们可以通过父亲关系来寻找它的真后缀。
由于真后缀的 right 集合是逐渐扩大的,因此,如果某一个状态 p 是last 的真后缀状态,并且已经具有了边 a[i] ,那么状态 p 的真后缀状态也具有了边a[i] ,并且都转移到同一个状态 q 。由于状态p 是 last 的真后缀状态,那么两者同时添加一个字符 a[i] ,得到的状态也具有同样的真后缀形态,那么现在我们知道,状态 q 一定是状态np 的真后缀状态。注意这里的 q 最大长度是Max(p)+1 ,也就是上文所述分裂出来的新状态。
构造完成!实现
inline void build_suffix_automation(char *str) { statepool[++cnt] = cur = root = new state(0); for (char *s = str; *s; s++) { int x = *s - 'a'; state *p = cur; cur = statepool[++cnt] = new state(s - str + 1); for (; p && !p->trans[x]; p = p->fa) p->trans[x] = cur; if (p == NULL) cur->fa = root; else { state *q = p->trans[x]; if (q->len == p->len + 1) cur->fa = q; else { state *nq = new state(0); statepool[++cnt] = nq; nq->len = p->len + 1; memcpy(nq->trans, q->trans, sizeof q->trans); nq->fa = q->fa; cur->fa = q->fa = nq; for (; p && p->trans[x] == q; p = p->fa) p->trans[x] = nq; } } } }
- 如果
Max(q)=Max(p)+1
,那么
q
状态现在代表的字符串没有任何需要变动,只是需要在
- 对于
a
的每一个子串