SAM拾遗碎记
SAM是一个非常复杂的算法,相关到很多本质性的问题需要思考,但受限于个人能力,想要完整而系统的写一篇学习笔记,对我来说绝非易事。虽然如此,又不能完全不写点什么,我上次好不容易学完了sam,这次又再一次花了很长的时间,并且还是在之前有所记录的情况下。正是因为sam是如此难的一个算法,每一次新的领悟和理解都是需要一定时间和巧合的,所以我还是选择零碎记下一些关键之处,以备下次学习时能有一些方向和启发。
我之前的博客说,sam是对于有相同endpos的一系列子串做了一个压缩,所以很多功能也建立在endpos相同的子串具有许多相同的性质。这是很有道理的,可以作为sam的一个本质来思考。
sam是一个子串数据结构,每一个子串都能在其中找到对应的表示,子串是连续的,这是一个关键的性质,能够将\(n^2\)的子串数压缩在\(O(n)\)的数据结构中,本质上就是利用了子串连续的性质,当然,如何具体,定量的分析,就是我一直想要搞明白但又还没有的工作了,依然是复用理论中信息复杂度的问题,任重道远啊。
子串,一个特定的子串其实就是一个特定的前缀的特定后缀,这就是sam如何维护所有子串的信息了,sam通过从前往后一个一个的添加字符,从前往后就是所有的前缀, 对于每个前缀,又去维护所有的后缀。
所谓endpos ,其实就是一个子串的出现位置集合,如果两个子串的所有出现位置完全相同,那么其中一个串必然是另一个的后缀。
对于一个endpos,一个字符串出现的所有位置,如果把这些所有的不同位置的相同字符串同时向前扩展一个字符,如果这些扩展的字符都相同, 那么扩展后的字符串都相同,这个字符串有这么一种唯一的向前扩展方式,所以这个字符串在且仅在原来那个endpos中的所有位置出现,所以一个endpos,会覆盖一个字符串区间,直到这个区间中最长的字符串向前扩展时,出现了不同的扩展方式,即不同的扩展字符,这时候就会出现endpos分裂。
endpos构成一颗树, 这个树的所有叶子节点代表一个前缀,因为叶子节点的意思是无法再向前扩展了,只有前缀无法向前扩展,但前缀不一定是叶子节点,因为一个前缀虽然无法再向前扩展了, 但这个子串可能不止在前缀这一个地方出现过,其他非前缀的出现位置仍然可以继续扩展,所以它的分裂出去的endpos大小和会比原来endpos少一个,这一个就是前缀。
简单讲一下如何维护sam把,还是复用的思路,sam的核心是维护后缀link,我们考虑每次往后加入一个新的字符,这时出现了一个新的后缀,如何复用前面的信息来得到这个新的后缀呢?我们考虑这个后缀除了最后一个字符的的子串,也就是前一个后缀,新的后缀可以由前一个后缀向后扩展一个字符得到, 那么我们记录一个ch数组,这个数组用于向后扩展字符,但意义有所不同,这个数组表示的是,严格包含扩展后字符串,且以该串为后缀的,包含且仅包含该串所有出现次数的endpos, 只要这个串之前出现过就会有这么个endpos, 所以你加入新字符后,这个新的最长后缀的某些比较长的子后缀可能是之前没有出现过的,是第一次出现的,所以你要跳跳跳,跳到一个之前出现过的, 但之前出现的所有位置可能都是带着一些更多字符同时出现的,但你现在决定加入的这一个很有可能不顺便带有那些字,符,这就是为什么你要分裂它了。关于构建的部分我就不多讲解了, 这涉及到许多复杂度的问题, 我自己也没搞懂,下次在研究。
我们要维护所有子串的信息,在树上做dp,关键是要包含所有子串,这时正确性的保证,求出endpos出现的所有位置可能是一个具有典型性的问题, endpos其实就是一个后缀,endpos出现的所有位置,其实就是以endpos为后缀的所有子串,也即是所有可能扩展出现的位置,它就等于所有儿子的合并,以及可能存在的本身也代表一个字符串情况, 每一个儿子是每一种扩展,如果这个endpos本身是一个前缀,那么这个不可扩展的前缀要特别记录在当前节点中,这就是一个不重不漏的正确性保证。这个东西或许可以用什么线段树合并来维护。
后缀link构成的那颗树,可以理解为一颗trie树, 是往前加字符的。其实ch,也就是转移, 也可以理解为一个往后加字符的trie树,这就是sam同时支持前后加字符的很好功能,需要注意的是,这个向后的trie树是被压缩的(虽然向前也压缩,但不破坏树结构),这就是为什么它的多个点被压成一个点从而形成了一个dag。
标签:子串,前缀,SAM,后缀,扩展,拾遗,endpos,sam,碎记 From: https://www.cnblogs.com/ltdjcoder/p/18335387