SAM
如果在一个 $DAG$ 上表示一个字符串的所有子串,朴素的想法是将其所有后缀加入 $Trie$ 中,然而这样节点数是 $O(n^2)$ 的。可以发现,图中节点有很多可以合并之处。
首先是后缀本身。如果先不考虑后缀的公共前缀的存在,那么根本没有必要对每一个后缀都重新存储,可以直接用一条链表示原串,再从根指向各个节点,这样就合并了各后缀,因为只要跳过了开头的一段就可以得到一个后缀。这相当于是 $Trie$ 是菊花图时,把那些后缀拉过来,强行与最长的后缀(即原串)合并,保留那些与根相连的边。图中原串为 abc
。
然而后缀可能存在公共的前缀,这使得 Trie 不是菊花图。这样按之前的做法,可能有一个子串出现多次。这不利于解决问题,不是我们想要的。因此 SAM 的一个限定条件是每一个子串都恰好出现一次。
考虑之前做法的本质。由于新加入一个字符 c
时该字符从未出现过,因此增加的子串为所有原有后缀加上 c
和 c
本身,即新的后缀。但如果 c
之前已经出现过,就会出现某些后缀之前出现过,就不能这样加边了。
现在假设我们建好了前 $n$ 个字符的 SAM,现在又加入了一个字符 c
。首先一定会与之前的末尾字符连边,这样就有了整个串这个子串。此时新增添的子串一定是原有的能到达的后缀(即,这些后缀是在 SAM 中以到达末尾节点得到的子串,这个后缀除了在后缀的位置上外没有另外出现过)加上了 c
,不可能在 SAM 中出现过,否则去掉这个字符一定也出现过。考虑那些之前出现过,但加上 c
后首次出现的后缀。
如果加入之后较大的后缀出现过,较小的后缀就一定也出现过了。因此从大到小考虑这些后缀(此处为原串后缀)。如果某个后缀对应的节点没有 c
的边就连边,否则就停止这一过程。
那么如何考察这些后缀呢?
由于每个子串至多对应一个节点,因此可以在每一个节点存储一个指向上一个子串节点的指针,其指向的节点所表示的子串为与当前节点子串由不同节点表示的后缀中最长的一个,而更短的后缀将递归得到。