后缀自动机SAM
一些记号:
- \(|s|=len(s)\)
- \(t_0\):初始状态
- \(endpos(t)\):字符串\(s\)中子串\(t\) 的结束位置的集合,集合大小就是\(t\)在\(s\)中出现的次数
- \(link(v)\): 状态\(v\)的后缀链接
- \(len(v)\):状态\(v\)对应的最长子串的长度
- \(minlen(v)\):状态\(v\)对应的最短子串的长度
一些名词:
- 后缀链接树:\(v\to link(v)\)形成的树
- 前缀节点:一个节点\(v_i\)的所代表的最长字符串是原字符串的一个前缀,则称\(v_i\)为前缀节点
可以解决的问题:
- 在另一个字符串中搜索一个字符串的所有出现位置
- 计算给定的字符串中有多少个不同的子串
- 在另一个字符串中判断一个字符串是否出现过
- 计算给定的字符串中所有不同子串的总长度
- 字典序第 k 大子串
- 字符串的最小循环移位
- \(\cdots \cdots\)
endpos:
两个子串\(t_1\)和\(t_2\)的结束位置可能完全相同:endpos(t1)=endpos(t2),把endpos相同的非空子串叫做一个等价类。
性质1:如果有两个非空子串\(v,w\)(\(|v|\le |w|\))的endpos相同,则\(v\)是\(w\)的后缀
性质2:endpos相同的子串的长度是连续的
后缀链接link:
\(v\)的后缀链接\(link(v)\)连接到的状态,对应于\(w\)的后缀中与它的endpow集合不同且最长的那个
性质1:所有后缀链接构成一棵根节点为\(t_0\)的树。(每一个点的后缀链接会连接到\(len\)严格小于该节点的点,最终一定会到达\(t_0\))
性质2:后缀链接树上的父节点的endpos集合一定包含其儿子节点。
性质3:后缀链接树每一个节点包含的子串出现的次数,就是该
构造SAM:
假设现在已经构造了字符串\(s\)的SAM,最后插入的字符所对应的状态为\(last\),现在要插入一个字符\(c\)(设在后缀链接的编号为\(cur\)),那么从\(last\)开始,判断该节点是否有字符\(c\)的出边,如果没有,就跳该节点的后缀链接,继续判断,如果有,则要分类讨论:(设当前跳到了\(parent\)树上的点\(p\), 点\(p\)经由字符串\(c\)转移到了点\(q\))
- 情况一:$len(p)+1=len(q) \(,此时点\)q\(代表的字符串就是点\)p\(代表的字符串加上\)c\(,直接把\)cur\(连接到\)q$即可
- 情况二:$len(p)+1\ne len(q) \(,此时点\)q\(代表的字符串集合大于点\)p\(代表的字符串加上\)c\(的集合,所以不能直接把\)cur\(连接到\)q\(即可。此时只能把点\)q\(克隆一个出来,分成两个,一个是\)clone$ , $len(clone)=len(p)+1 \(,\)clone\(除了\)len\(的值其他都是\)q\(的, 另一个就是点\)q'\(,和\)q\(代表的字符串是一样的,接下来操作是把\)p\(到\)t_0\(的路径上的点所有从字符\)c\(转移到\)q\(的边都变成转移到\)clone\(边,最后把\)cur\(和\)q\(的后缀链接连接到\)clone$上
做完这些以后把\(last\)设为\(cur\)即可。
具体过程核心代码:
void insert(char c) {
int cur = ++ sz;
int p = last;
sam[cur].len = sam[p].len + 1;
while(p != -1 && !sam[p].nxt.count(c)) {
sam[p].nxt[c] = cur;
p = sam[p].link;
}
if(p == -1) {
sam[cur].link = 0;
}else {
int q = sam[p].nxt[c];
if(sam[q].len == sam[p].len + 1) {//情况一
sam[cur].link = q;
}else {//情况二
int clone = ++ sz;//克隆出一个点
sam[clone].len = sam[p].len + 1;
sam[clone].nxt = sam[q].nxt;//把clone的其他值都设为q的
sam[clone].link = sam[q].link;//与上同理
while(p != -1 && sam[p].nxt[c] == q) {// 改边
sam[p].nxt[c] = clone;
p = sam[p].link;
}
sam[q].link = sam[cur].link = clone;
}
}
last = cur;
val[cur] = 1;
}
注意:
- \(clone\)和\(q\)的出边一样,是因为\(clone\)和\(q\)合起来是原来的\(q\),所以\(clone\)和\(q\)的出边和原来的\(q\)是完全一样的,所以在代码上就是直接把\(clone\)的边复制成\(q\)的就行。
- 把\(p\)在\(parent\)树上的父节点的经过\(c\)的转移边都改到\(clone\)上,是因为将原来的点分裂,实际上是分成了一个所有字符串长度\(\le len(p) + 1\)(就是\(clone\))的和一个所有字符串长度\(>len(p)+1\)的两个点,而\(p\)在\(parent\)树上的父节点的\(len< len(p)\),所以经由\(c\) 转移后的\(len + 1 < len(p) + 1\),所以应该连向\(clone\)
- 因为分裂后的\(q\)和\(clone\)刚好能够拼起来,所以他们所包含的字符串的长度是连续的,也就是说$minlen(p)+1=len(p) \(,根据后缀链接树的定义,\)q\(因该连接到\)clone\(,而\)cur$是 \(last+c\)的得到的,又因为\(p\)是\(last\)的合法后缀,\(clone\)是\(p+c\)得到的,所以\(clone\)是\(cur\)的后缀,所以\(cur\)应该连接到\(clone\)。
由此能够解释构造过程中的具体操作了。
应用:
不同字串个数:建出来\(parent\)树后计算每个节点\(len(p) - len(link(p))\)之和即可
不同字串的长度总和:计算\(parent\)树上每个节点\(\sum_{i=minlen(p)}^{len(p)}i\)
文本串出现次数:设这个字串在\(parent\)树上的节点为\(v\),则这个子串的出现次数\(=endpos(v)\)集合大小\(=v\)子树内前缀节点的数量
\(\cdots\cdots\cdots\cdots\cdots\cdots\cdots\)
空间复杂度:
因为最多分裂\(n-1\) 次,一共有\(n\)个字符,所以在\(sam\)和\(parent\)树上一共有\(2n-1\)个点

浙公网安备 33010602011771号