学习笔记——SAM

前言

不想学博弈论不想学 SA 不想学插头 dp,学 lct 被 AxDea D 飞了,那就来学 SAM。

SAM?

SAM 是后缀自动机,名义上是后缀,但实际上它能表示出一个字符串的所有不同子串。不同于你的 \(O(n^2)\) 枚举,SAM 构造,节点和边的数量也都是 \(O(n)\) 级别的。

更具体的,SAM 表现为一张 DAG,每条边上有一个字母(类似于 AC 自动机和 trie),然后从源点开始到任意节点结束都能形成一个字符串,而这些字符串刚好都是原来字符串的子串,并且已经包含了所有的子串。

前置的定义

在构造 SAM 之前,需要知道一些定义。

endpos

我们对于一个字符串 \(S\) 的任意一个子串 \(T\),那么 \(T\) 在 \(S\) 中所有出现的位置的 结束位置 下标的集合叫做 \(T\) 的 endpos。

  • 例:对于串 \(\texttt{aabaabbcbbaab}\),那么子串 \(\texttt{aab}\) 的 endpos 是 \(\{3,6,13\}\)。记作 \(\operatorname{edp}(\texttt{aab})=\{3,6,13\}\)。

endpos 等价类

我们把 endpos 相同的串的集合称作一个 endpos 等价类,简称类。

  • 例:对于串 \(\texttt{aabaabbcbbaab}\),\(\{\texttt{ab},\texttt{aab}\}\) 是一个类,因为它们的 endpos 都是 \(\{3,6,13\}\)。

前置的引理

在构造 SAM 之前,需要知道一些引理。下面我们认为 \(S_1\) 和 \(S_2\) 是原串 \(S\) 的两个不同子串,并且有 \(|S_1|\le |S_2|\)。

Lemma 1.1

\(S_1\) 是 \(S_2\) 的后缀 \(\Rightarrow\) \(\operatorname{edp}(S_2)\subseteq\operatorname{edp}(S_1)\)

  • 很好理解,既然 \(S_1\) 是 \(S_2\) 的后缀,那么有 \(S_2\) 的地方就有 \(S_1\)。

Lemma 1.2

\(\operatorname{edp}(S_2)\cap\operatorname{edp}(S_1)\not=\varnothing\) \(\Rightarrow\) \(\operatorname{edp}(S_2)\subseteq\operatorname{edp}(S_1)\),且 \(S_1\) 是 \(S_2\) 的后缀

  • 如果两个串的 endpos 有交,那么其中一个必然是另一个的后缀,然后由 Lemma 1.1 推出 \(\operatorname{edp}(S_2)\subseteq\operatorname{edp}(S_1)\)。

  • 这条定理说明每个字符串之间 endpos 没有交,只能是相互包含的关系。

Lemma 1.3

在一个类中,找出其长度最大的串 \(u\),那么剩下的串长度从 \(u\) 开始递减。

  • 例:对于串 \(\texttt{abcdabcdd}\),在 \(\operatorname{edp}=\{4,8\}\) 类中有 \(\texttt{abcd,bcd,cd}\),可以看到长度是递减的。

  • 怎么证明?假设其中最长的串是 \(MaxS\),最短的是 \(MinS\),那么对于 \(MaxS\) 的任意一个长度大于 \(|MinS|\) 的后缀 \(Suf\),由 lemma 1.2 可以知道,这个 \(Suf\) 一定是 \(MaxS\) 的后缀,\(MinS\) 一定是 \(Suf\) 的后缀。用 Lemma 1.1 推得 \(\operatorname{edp}(MaxS)\subseteq\operatorname{edp}(Suf)\subseteq\operatorname{edp}(MinS)\),那由于 \(\operatorname{edp}(MaxS)=\operatorname{edp}(MinS)\),所以 \(\operatorname{edp}(MaxS)=\operatorname{edp}(Suf)=\operatorname{edp}(MinS)\),也就是说,\(Suf\) 一定和 \(MaxS,MinS\) 在同一类。

Parent Tree

根据 Lemma 1.2 可以发现,这些子串之间形成了一棵树形结构。我们把这个树形结构叫做 Parent Tree。

更具体地,考虑一个类中最长串 \(MaxS\),如果我们向这个串的前面加入一个字符,得到一个字符串 \(NewS\),那原来类中元素的 endpos 中的位置分成了两种,一种是在 \(\operatorname{edp}(NewS)\) 中的位置,一种是不在其中。然后我们在 \(MaxS\) 前面加上不同字符就把 endpos 中的位置分成了 \(\sum\) 种(\(\sum\) 是字符集),作为这个类的儿子节点(空的删掉),然后就构成了这棵 Parent Tree。

一棵 Parent Tree

学习笔记——SAM

一些说明:上面节点中的 \(s\) 表示这一类中的 \(MaxS\),也就是最长的串。

反正还是看图理解吧。那这样一棵树有什么用呢?其实在后面给出 SAM 的构造的时候会说,我们对于一个节点需要能够找到它在 Parent Tree 上的父亲。而且 SAM 上节点的意义和 Parent Tree 节点的意义是相同的,使得其节点数也和 Parent Tree 相同,从而保证复杂度(见下方引理)

其实不难发现通过枚举在 \(MaxS\) 前面加的点来构造这棵树的过程,是能够不重不漏地表达出所有原串的子串的,这和 SAM 的性质相同。

所以 SAM 是基于 Parent Tree 构造的。

接下来有几个引理:

Lemma 2.1

对于 Parent Tree 上的节点 \(u\) 和它的一个儿子 \(v\),\(u\) 中的 \(MaxS\) 的长度加一就是 \(v\) 中的 \(MinS\) 的长度。

  • 很好证明,我们通过在 \(MaxS\) 前面加一个字符的方式产生 \(u\) 的儿子,那么这个 \(NewS\) 属于 \(v\)。然后易得长度小于 \(NewS\) 的串不在 \(v\) 中(它们的 endpos 要么不是 \(v\) 所代表的,要么不止 \(v\) 所代表的)。

Lemma 2.2

Parent Tree 的点数和边数都是 \(O(n)\) 级别的。

  • 这个东西我不会证,但是会感性理解,具体可以看底下的的参考文献~

SAM 的构造

先来看代码吧。

struct node{int ch[26],fa,len;}tr[MAXN<<1];
int tot,lst;
void ins(int c){
	int p=lst,np=++tot;lst=np;
	while(p&&!tr[p].ch[c]) tr[p].ch[c]=np,p=tr[p].fa;
	if(!p) tr[np].fa=1;
	else{
		int v=tr[p].ch[c];
		if(tr[v].len==tr[p].len+1) tr[np].fa=v;
		else{
			int nv=++tot;tr[nv]=tr[np];
			tr[nv].len=tr[p].len+1;
			while(p&&tr[p].ch[c]==v) tr[p].ch[c]=nv,p=tr[p].fa;
			tr[v].fa=tr[np].fa=nv;
		}
	}
}

首先,SAM 是用增量法构造的,就是通过在后面加入一个字符,然后插入新字符串的所有后缀。这样最终必然能表出原串的所有子串。

然后我们考虑插入这些后缀的时候会对 Parent Tree 上哪些节点产生影响。很容易可以想到,就是节点表示的 endpos 中包含了原来的长度的那条链。比如,对于上面那个图,\(\{1,2,3,4,5,6,7\}\to\{1,2,4,5,7\}\to\{4,7\}\to\{7\}\) 这一条链,在插入第 \(8\) 个字符的时候会有影响。我们把这条链叫做 终止链

然后我们考虑这多出来的一个字符会产生什么影响。首先对于自动机而言,我们从终止链底向上跳,每个节点都要向新建的节点连一条边上是字符 \(c\) 的边。那如果一路畅通,那么皆大欢喜,没什么影响,只要在 Parent Tree 的根上多连一个儿子就行了,相当于是出现了一个陌生的字符,这是第一种情况;第二种情况是如果我们向上跳的过程中有一个节点 \(p\),已经有边上是 \(c\) 的边连到另一个节点 \(v\) 了,说明有重复的子串了。那这个时候我们需要多维护一个东西来进一步地分讨。

我们维护每个节点的 \(MaxS\),代码中记为 \(len\)。在上面这种情况下,如果有 tr[p].len+1==tr[v].len,那么说明,在 Parent Tree 中,还没有比加字符产生的新后缀更长的,并且同样属于 \(v\) 的一个后缀。

上一篇:Memcached缓存,深入分析解读MySQL锁,解决幻读问题


下一篇:遗传算法GA优化支持向量机分类代码,优化c,g参数代码matlab