后缀自动机 SAM

The Link of My Video

十五分钟学会后缀自动机 SAM

定义

  • SAM 是 DFA 确定性有限状态自动机,是一张 DAG 有向无环图。结点为 状态 ,边被为状态间的 转移
  • 图存在一个虚拟结点 S S S ,称作 初始状态 ,其它各结点均可从 S S S 出发到达。
  • 每个 转移 都标有一些字母。从一个结点出发的所有转移均 不同 。从一个状态出发的转移标有的字母不同。
  • 存在一个或多个 终止状态 。如果从初始状态 S S S 出发,最终转移到了一个终止状态,则路径上的所有转移连接起来一定是字符串 s s s 的一个后缀。反之 s s s 的每数个后缀均可用一条从 S S S 到某个终止状态的路径构成。
  • SAM 包含关于字符串 s s s 的所有子串的信息。如果从初始状态 S S S 出发,最终转移到了一个状态,路上的所有转移连接起来都会形成 s s s 的一个 子串 。反之每个 s s s 的子串对应从 S S S 开始的某条路径。
  • 在所有满足上述条件的自动机中,SAM 的结点数是最少的。

SAM 可以理解为给定字符串的 所有子串 的压缩形式。值得一提的是,一个 SAM 最多有 2 n − 1 2n-1 2n1 个结点 3 n − 4 3n-4 3n4 条转移边。所以它的构造是线性的。

状态

对于字符串 s s s 的任意非空子串 t t t ,记 e n d p o s ( t ) endpos(t) endpos(t) 为在字符串 s s s t t t 的所有结束位置的集合。如 aabbabdab e n d p o s endpos endpos 集合为 { 3 , 6 } \{3,6 \} {3,6}

e n d p o s endpos endpos 相等的子串归于同一个 e n d p o s endpos endpos 等价类。而 e n d p o s endpos endpos 等价类就是 SAM 的状态。对于初始状态 S S S,可以看作只有一个空串的 e n d p o s endpos endpos 等价类。

我们可以得到一些结论。对于字符串 s s s 两个非空子串 s 1 , s 2 s_1,s_2 s1,s2,假设 ∣ s 1 ∣ < ∣ s 2 ∣ |s_1| < |s_2| s1<s2。约定 l o n g e s t ( x ) longest(x) longest(x) 为状态 x x x 中最长的子串。

  • 如果 s 1 s_1 s1 s 2 s_2 s2 同属于一个状态,当且仅当 s 1 s_1 s1 s s s 中的每次出现,都是以 s 2 s_2 s2 后缀的形式存在的。

  • s 1 s_1 s1 s 2 s_2 s2 的后缀当且仅当 e n d p o s ( s 1 ) ⊇ e n d p o s ( s 2 ) {endpos}(s_1) \supseteq {endpos}(s_2) endpos(s1)endpos(s2) s 1 s_1 s1 不为 s 2 s_2 s2 的后缀当且仅当 endpos ( s 1 ) ∩ e n d p o s ( s 2 ) = ∅ \text{endpos}(s_1) \cap {endpos}(s_2) = \emptyset endpos(s1)endpos(s2)=

  • e n d p o s ( x ) {endpos}(x) endpos(x) 中的子串按长度从大到小排序,所有的子串为的 l o n g e s t ( x ) longest(x) longest(x) 长度连续的后缀

后缀链接

endpos ( x ) \text{endpos}(x) endpos(x) 中的子串按长度从大到小排序,所有子串为的 l o n g e s t ( x ) longest(x) longest(x) 长度连续的后缀 。

但是,长度连续的后缀可能会断开。

aabbab 中, aabbababbabbbabbab 在一个状态中(状态 7 7 7)。下一个理应为 ab。但是 ab 在另一个状态 endpos = 3 , 6 \text{endpos} = {3,6} endpos=3,6 中(状态 8 8 8),对于 ab 的下一个后缀 b,它又在一个新的类 endpos = { 3 , 4 , 6 } \text{endpos} = \{ 3,4,6 \} endpos={3,4,6} 中(状态 5 5 5)。所以有 7 → 8 7 \to 8 78 8 → 5 8 \to 5 85有向绿边,这就是后缀链接。

具体的,一个状态 x x x 的后缀链接 l i n k ( x ) link(x) link(x) 连接到: l o n g e s t ( x ) longest(x) longest(x) 最长的后缀所对应的状态,满足该后缀为另一个状态中的最长子串

构造

在构造中维护以下信息:

  • l e n ( x ) len(x) len(x) longest ( x ) \text{longest}(x) longest(x) 的长度。
  • l i n k ( x ) link(x) link(x) x x x 的后缀链接。
  • n x t ( x , c ) nxt(x,c) nxt(x,c) x x x 的转移,其中 c c c 为字符。
const int N = 2e6 + 5; // 至少开数据范围的两倍空间
struct state {
  int len, link;
  int nxt[26]; // 26 为字符集的大小,如果字符集较大可以用 map
} st[N];
int sz, lst;

后缀自动机的构造是一个线性在线算法

初始,SAM 只有一个状态 S S S,它的编号为 0 0 0 l e n ( S ) = 0 , l i n k ( x ) = − 1 len(S) = 0, link(x) = -1 len(S)=0,link(x)=1

现在向 SAM 逐个加入字符,假设现在加入的字符为 c c c,之前加入的字符构成的字符串的状态为 l s t lst lst

之前加入的字符所构成的字符串 s s s 一定是 l o n g e s t ( l s t ) longest(lst) longest(lst),所以加入 c c c 生产的字符串 s + c s + c s+c 会有一个新的状态 c u r cur cur l e n ( c u r ) = l e n ( l s t ) + 1 len(cur) = len(lst) + 1 len(cur)=len(lst)+1

  • p p p 从从状态 l s t lst lst 开始,通过后缀链接 S S S 的方向走,也可以说成沿着 parent tree 往根节点走。对于路径上的状态 p p p,如果没有 n x t ( p , c ) nxt(p,c) nxt(p,c) 的转移,那么 n x t ( p , c ) = c u r nxt(p,c) = cur nxt(p,c)=cur

  • 否则将 l i n k ( x ) link(x) link(x) 赋值为 0 0 0 并退出。

  • 如果没有找到 p p p,那么到达了虚拟状态 S = − 1 S=-1 S=1,将 l i n k ( c u r ) link(cur) link(cur) 赋值为 0 0 0 并退出。

  • 否则,找到了一个状态 p p p,这个状态可以通过 c c c 转移到状态 q q q,那么有两个情况。

  • 如果 l e n ( p ) + 1 = l e n ( q ) len(p) + 1 = len(q) len(p)+1=len(q),这意味着 l o n g e s t ( p ) + c = l o n g e s t ( q ) longest(p) + c = longest(q) longest(p)+c=longest(q)。只要将 l i n k ( c u r ) link(cur) link(cur) 赋值为 q q q 并退出。

  • 否则略有复杂,新建一个克隆状态 c l o n e clone clone,复制 q q q 的后缀链接和转移,并将 l e n ( c l o n e ) len(clone) len(clone) 赋值为 l e n ( p ) + 1 len(p)+1 len(p)+1。复制之后,将 l i n k ( c u r ) link(cur) link(cur) l i n k ( q ) link(q) link(q) 赋值为 c l o n e clone clone。然后通过后缀链接从 p p p 往回走,对于路径上的状态 p p p,如果 n x t ( p , c ) = q nxt(p,c) = q nxt(p,c)=q,那么将 n x t ( p , c ) nxt(p,c) nxt(p,c) 重新定向到 c l o n e clone clone

最后,将 l s t lst lst 赋值为 c u r cur cur

void sam_extend(int c) {
  	int cur = ++sz;
  	st[cur].len = st[lst].len + 1;
  	int p = lst;
  	while (p != -1 && !st[p].nxt[c]) {
  	  	st[p].nxt[c] = cur;
  	  	p = st[p].link;
 	}
  	if (p == -1) st[cur].link = 0;
  	else {
    	int q = st[p].nxt[c];
    	if (st[p].len + 1 == st[q].len) st[cur].link = q;
    	else {
      		int clone = ++sz;
      		st[clone].len = st[p].len + 1;
      		for (int i = 0; i < 26; i++) st[clone].nxt[i] = st[q].nxt[i];
      		st[clone].link = st[q].link;
      		while (p != -1 && st[p].nxt[c] == q) {
        		st[p].nxt[c] = clone;
        		p = st[p].link;
      		}
      		st[q].link = st[cur].link = clone;
    	}
  	}
  	lst = cur;
}
int main() {
    st[0].link = -1;
    for (int i = 0; i < n; i++) sam_extend(s[i] - 'a');
    return 0;
}

正确性证明

假设之前的字符串 s s s 的构造是正确的。

通过后缀链接进行遍历,路径上的 p p p 一定是 s s s 不同状态的最长后缀所属的状态。如果没有转移,意味着在 SAM 中还没出现,所以可以转移到 c u r cur cur,否则已经出现了,为了不产生重复所以跳出。

当到达了虚拟状态 S = − 1 S = -1 S=1 时,这意味着对 s s s 的所有后缀添加了到 c c c 的转移了,所以 l i n k ( c u r ) link(cur) link(cur) 赋值为 S S S,代表断开的是一个空串。

否则找到了现有的转移 p → q p \to q pq。这意味着 s + c s + c s+c q q q最长公共后缀 x x x 已经在 SAM 中出现过了,不能添加新的转移了。而 l i n k ( c u r ) link(cur) link(cur) 应该连向 x x x 所在的状态。如果 x x x l o n g e s t ( q ) longest(q) longest(q),那么有 l e n ( p ) + 1 = l e n ( q ) len(p) + 1 = len(q) len(p)+1=len(q),所以将 l i n k ( c u r ) link(cur) link(cur) 赋值为 q q q

否则,在 q q q 中还有更长的子串即为 l e n ( p ) + 1 < l e n ( q ) len(p) +1 < len(q) len(p)+1<len(q),所以需要把 q q q 拆成两个子状态,第一个子状态中最长的子串为 x x x 它的长度为 l e n ( p ) + 1 len(p) + 1 len(p)+1,第二个还是 q q q,第一个子状态即为 c l o n e clone clone 克隆状态。为了不改变到 q q q 的路径,所以将连向 q q q 的转移改接到 c l o n e clone clone 上,并将 l i n k ( q ) link(q) link(q) 赋值为 c l o n e clone clone。那么 l i n k ( c u r ) link(cur) link(cur) 也是 c l o n e clone clone 了。

后缀链接树 / parent tree

不是后缀树,是后缀链接树。

SAM 中后缀链接的反向边构成了一棵以 S S S 为根的。这棵树有一些性质

  • 如果状态 x x x 是状态 y y y 的祖先,那么 x x x 中字符串为 y y y 中字符串的后缀。
  • S 1 ∼ p S_{1\sim p} S1p S 1 ∼ q S_{1 \sim q} S1q 的最长公共后缀为 l o n g e s t ( L C A ( p , q ) ) longest(LCA(p,q)) longest(LCA(p,q))
  • 状态 x x x 中本质不同的子串的个数为 l e n ( x ) − l e n ( f a ) len(x) - len(fa) len(x)len(fa)
  • 状态 x x x 中每个子串在原串出现的次数为以 x x x 为根的子树中非克隆状态的大小之和。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值