SAM 可以理解为给定字符串的 所有子串 的压缩形式。值得一提的是,一个 SAM 最多有 2 n − 1 2n-1 2n−1 个结点 3 n − 4 3n-4 3n−4 条转移边。所以它的构造是线性的。
对于字符串 s s s 的任意非空子串 t t t ,记 e n d p o s ( t ) endpos(t) endpos(t) 为在字符串 s s s 中 t t t 的所有结束位置的集合。如 aabbabd
中 ab
的 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
中, aabbab
、abbab
、bbab
、bab
在一个状态中(状态 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 7→8 和 8 → 5 8 \to 5 8→5 的有向绿边,这就是后缀链接。
具体的,一个状态 x x x 的后缀链接 l i n k ( x ) link(x) link(x) 连接到: l o n g e s t ( x ) longest(x) longest(x) 最长的后缀所对应的状态,满足该后缀为另一个状态中的最长子串。
在构造中维护以下信息:
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 p→q。这意味着 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 了。
SAM 中后缀链接的反向边构成了一棵以 S S S 为根的树。这棵树有一些性质