后缀字符串杂录
字符串-后缀总结
众所周知后缀相关的字符串数据结构有两种:后缀自动机和后缀树
后缀数组邪教东西
总结一下它们的性质与联系,以供今后复习
下面有的名称是我瞎编的,比如 SUT 和 occ 等价类
零
解释一些基础的东西用的
先咕着
一
对于一个子串(包括空串),把它在原串中所有出现的结束的位置的集合称为它的 \(\rm endpos\) 集合
每个子串只对应一个 \(\rm endpos\) 集合,所以可以将所有子串按照 \(\rm endpos\) 集合来分类,\(\rm endpos\) 集合相同的分一类
这里称其为 \(\rm endpos\) 等价类,简称等价类
二
讨论同一个 \(\rm endpos\) 等价类中的所有子串的关系
对着一个长度为 \(i\) 的子串 \(A\) 看,在原串中找到它出现的所有位置,分别往前加原串中出现在它前面的那些字符,扩展成长度为 \(i+1\) 的子串
若往前加的所有字符都相同,那么这些长度为 \(i+1\) 的子串都一样,与 \(A\) 属于同一个等价类,且 \(A\) 是它的后缀
若不同,那么这些长度为 \(i+1\) 的子串会根据这些字符来分成不同的字符串,并且被划分为其它的新等价类中,并瓜分掉 \(A\) 的 \(\rm endpos\) 集合
上述过程从 \(1\) 重复几遍,就可以得到所有子串
感性理解一下就可以得出结论:
一个等价类中的所有串的长度连续,且是其中的最长串的后缀
三
不同的 \(\rm endpos\) 集合之间只有两种关系:包含与不交
这个由第二节的过程是不难证明的
这暗示着所有 \(\rm endpos\) 集合之间可以构成一棵树
这棵树每个节点表示一个 \(\rm endpos\) 集合,每个节点的祖先表示包含它的其它 \(\rm endpos\) 集合
这棵树叫做 \(\rm parent\) 树
四
讨论一个节点的父亲与它的关系
意识到这个节点是由它的父亲通过第二节的过程“分裂”而来的
所以不难得出:
该节点的等价类中的最长串的长度是父亲的最短串的长度减 \(1\) 且是它的后缀
五
讨论 \(\rm parent\) 树节点中的一些特别的节点
空串对应 \(\rm endpos\) 集合是全集,为 \(\rm parent\) 树中的根节点
然后叶节点所对应的 \(\rm endpos\) 集合无法按第二节的步骤“分裂”,所以里面包含前缀
反之非叶节点都可以分裂但不一定含有前缀,而它们的 \(\rm endpos\) 集合是它的儿子的 \(\rm endpos\) 集合的并,加上当前集合中的前缀(如果有的话)
而一个字符串只有 \(n\) 个前缀,这代表着 \(\rm parent\) 树的叶子数是线性的,所以 \(\rm parent\) 树的节点数也是线性的
这样出题人就能搞很多很多事情了……到时候再提
六
后缀自动机,缩写为 \(\rm SAM\),以最大程度压缩了原串中的所有子串
既然是一个自动机,那么一定有状态和转移边
状态和 \(\rm parent\) 树中的节点定义一致即 \(\rm endpos\) 等价类,这样就能以线性的时间空间压缩所有子串
七
第二节的过程是往前面加字符的,那往后面加呢?
显然往后面加不同的字符也能到达另外一个不同的 \(\rm endpos\) 集合,不过不是直接分裂而是后移一位再分裂
这就是 \(\rm SAM\) 的转移,转移边上的字符就是往后面加的字符
它有一个神奇的性质
因为 \(\rm SAM\) 是一个 \(\rm DAG\),所以它从起始状态到某个状态的所有路径上的字符连起来形成的字符串都不同
而这个字符串一定是原串的一个子串,而且在路径的终止节点的等价类中
这样子出题人又能出一大堆题了
八
后缀树,是一个串的所有后缀的 \(\rm trie\) 树,这里简称 \(\rm SUT\),别的地方叫啥我不管了
直接按定义建 \(\rm SUT\) 节点数是平方极的,所以一定要压缩
怎么压缩呢?注意到我们所关心的只有一些关键点即代表后缀的点,这些点只有线性个,所以把关键点拿出来建一棵虚树就行了,时间空间都是线性的
不难发现这样只会压掉只有一个儿子的节点,这样虚树上的每条边都代表着一个字符串
九
\(\rm SUT\) 上的一个节点到根节点所连成的串是原串的一个子串且1这样可以唯一地表示所有子串
因为任意子串都为一个后缀的前缀
后面那个性质必须保证没有压缩过
但是我们只有压缩过的 \(\rm SUT\) 怎么办?
那么让一个点代表很多子串就行了,表示被个点指向父亲的边压缩掉的点表示的那些子串
发现这些子串是长度互不相同且连续的,最长串是其它串的前缀
是不是很耳熟?
和 \(\rm endpos\) 等价类的性质几乎一模一样,只是后缀改成了前缀,干脆叫它 \(\rm beginpos\) 吧
所以有一个重要的性质:反串的 \(\rm parent\) 树就是原串的 \(\rm SUT\)
十
(我当年写了啥?这段看不懂就跳过吧,我也看不懂了)
(所以我注释掉了直接)
(好像是当年某道校内题要用的东西……)
十一
关于区间 \(\rm SAM\)
有的出题人比较无良,搞个字符串题还要套一个区间上去
但是我们可以保留在区间内的所有字符串与其所对应的节点和有关的转移边
然后发现这些点还是可以构成一个合法的自动机
具体做法是用线段树合并维护 \(\rm endpos\) 集合然后判断一下
但是 \(\rm parent\) 树此时就保不住了,硬是要用 \(\rm parent\) 树的话……
放第十三节再讲
十二
关于建这两个结构
首先建 \(\rm SAM\) 的时候用 \(\rm Blumber\),还可以顺便把 \(\rm parent\) 树建出来
存边两种方式,开一个字符集大小的数组或者开 map
对应着复杂度多一个 \(|\sum|\) 还是多一个 \(\log|\sum|\)(\(\sum\) 是字符集)
要是出题人都卡那 \(\rm SAM\) 就 GG 了,只能老老实实用 \(\rm SA\) 或其它非后缀的东西
建 \(\rm SUT\) 可以用 \(\rm Ukkonen\) 或者反串 \(\rm parent\) 树,和上面的一样,也是多一个 \(|\sum|\) 或多一个 \(\log|\sum|\)
而且既然本质是棵 \(\rm trie\),那么自然也有 \(\rm AC\) 自动机的后缀链接
这个 \(\rm Ukkonen\) 还可以顺带把这个建了
具体构造方法不细讲,可以去看 cmd 的 blog
十三
要 \(\rm parent\) 树的区间问题
之前写的时候还不会
现在会了
\(\rm parent\) 树上的节点有三种:分裂节点、前缀节点和前缀分裂节点
概念就是五节里面的东西,含有前缀的就是前缀节点,有子节点的就是分裂节点,两个都有的就是前缀分裂节点
子串的 \(\rm parent\) 树的分裂节点且非后缀分裂节点是会与原串的 \(\rm parent\) 树产生对应关系的,而前缀节点和前缀分裂节点不一定
然后可以用 \(\rm LCT\) 和 \(\rm hash\) 加上二分计数,容斥计算三种节点
这样子就可以做一堆题了
具体可以看 18 年论文
十四
关于广义 \(\rm SAM\)
就是多串 \(\rm SAM\),让每个节点可以代表多个串中的子串
市面上流传着四种做法:
每个字符串中间隔一个特殊字符拼成一个字符串
\(\rm Blumber\) 时每次把 lasn 设成初始节点再把每个串插进去
建棵 \(\rm trie\) 按 \(\rm dfs\) 序插入每个点并每次把 lasn 设成父亲代表的点
建棵 \(\rm trie\) 按 \(\rm bfs\) 序插入每个点并每次把 lasn 设成父亲代表的点
最后那个是正派,其他有的复杂度是错的有的正确性是错的有的在特殊情况下是对的
十五
提一下后缀数组(简称 \(\rm SA\))
一个经典问题:求两个子串的 \(\rm LCP\)
这东西的流传千古的做法是用 \(\rm SA\) 和 \(\rm height\) 数组用各种结论来维护
然而用 \(\rm SUT\) 的话就会发现这东西就是两个点的 \(\rm LCA\)
说到这里还是要提一提 \(\rm SA\) 和 \(\rm SUT\) 的关系的
然后会发现把 \(\rm SUT\) \(\rm dfs\) 一遍就是 \(\rm SA\)
然而 \(\rm SUT\) 又可以由 \(\rm SAM\) 建出来
所以两个只要你会了一个就可以直接求 \(\rm SA\)
但是众所周知建 \(\rm SA\) 的复杂度与字符集是无关的还有线性做法
所以有出题人卡这个的话……
喷他就行了
除非板子题(说实话我到现在还没过 \(\rm SA\) 板子)
十六
好的我现在过了 \(\rm SA\) 板子了
因为它原来卡空间,改一下 \(\rm SAM\) 就过了
因为有结论:\(\rm SAM\) 的点数上限为 \(2n-1(n\ge 2)\),边数上限为 \(3n-4(n\ge 3)\)
感性理解是 \(\rm SAM\) 的 \(\rm Blumber\) 算法每次最多加两个点,所以点数不超过 \(2n\)
另外一个理解是上面讲 \(\rm parent\) 树的时候提过的线性证明,缕一缕会发现这个上限
再把每个点的 \(\rm endpos\) 集合中最长的字符串拿出来,可以根据转移关系构成一棵生成树,这部分边不超过 \(2n\)
其他边可以唯一代表一个后缀,所以这部分边不超过 \(n\)
加起来边数不超过 \(3n\)
(感性理解为啥要管常数)
理性证明可以去 oi-wiki 上面看
所以用邻接表存边就可以把 \(\rm SAM\) 空间压到线性
我还不会 \(\rm Ukkonen\),不知道 \(\rm SUT\) 可不可以做
十七
这文章时间跨度有点久远啊……
xtq 在它的论文里提出了一个很 nb 的玩意,他命名为基本子串结构
首先把所有子串按出现位置分类,设出现位置相同的在同一个 occ 等价类里
但是这句话过于抽象了,毕竟“出现位置”这个概念不能等价于第一个字符或者最后一个字符的出现位置,否则形成的结构和 SAM 的 parent 树和 SUT 没有区别,而且不能简单地用相邻出现位置的差形成的序列,否则在 abacded 这种串上 a 和 d 会在一个 occ 等价类上,性质不好
考虑 ARC151E 这个题一样的思路,如果对于一个串和将其后面加一个字符或减一个字符后得到的串,它们 beginpos 集合相同,或者对于一个串和将其前面加一个字符或减一个字符后得到的串,它们 endpos 集合相同,那么设它们在同一个 occ 等价类内
十八
考虑这样得到的 occ 等价类的性质
考虑两个串,如果它们在同一个 occ 等价类内,且两个串第一次出现的位置有重叠,那么把它们并起来得到的串也一定会在这个 occ 等价类内,所以会发现这样一个 occ 等价类内最长串唯一,称为它的 rep
那么一个 occ 等价类内的所有串都是它 rep 的子串,且仅在 rep 内出现一次,其 beginpos 和 endpos 集合根据其出现位置由 rep 的平移而来
画个 \(n\times n\) 的表格,第 \(i\) 行(从上到下)表示左端点为 \(i\) 的子串,第 \(i\) 列(从左到右)表示右端点为 \(i\) 的子串,那么表格内仅有右上半边的三角内的子串合法,然后只考虑 occ 等价类内每个子串第一次出现的位置
由于上面的性质,occ 等价类内的串都在 rep 左下方,而且会发现对于一个 occ 等价类内的串,它和 rep 之间的串都在这个 occ 等价类内
那么意味着这个 occ 等价类内的所有点会构成一个阶梯状网格图,即从上往下每行右端对齐,长度非严格下降
然后上面只考虑了第一次出现的位置,要找到所有位置可以把它整体往右下平移到相应位置
这样就把整个三角划成了若干阶梯
由于一个 occ 等价类相应位置的 rep 一样,所以它们左下角的划分都是一样的
十九
考虑这个结构的性质
会发现同一个 beginpos 或 endpos 等价类内的串都在同一个 occ 等价类内,换句话说,一个阶梯内的行表示了一个 beginpos 等价类,且列表示了一个 endpos 等价类
对于第 \(i\) 行,这些串的 beginpos 集都包含 \(i\),也就是第 \(i\) 行从左往右走可以遇到 SUT 上第 \(i\) 个后缀对应的点到根的链上的所有点,而且换一个阶梯就往上跳一个点
相应的,第 \(j\) 列对应了 SAM 的 parent 树即反串 SUT 上的一条对应点到根的路径
SAM 的每条边表示指定 endpos 等价类内的所有串后面加上指定字符后都会到达另一个指定的 endpos 等价类,对应过来就是每个阶梯的内部每列会往右边那列连,而最右边那列会直接连出去,而且会连向一整个 endpos 等价类,列上也有相似概念,对应反串 SAM

浙公网安备 33010602011771号