后缀自动机 (SAM)
后缀自动机
定义
定义 SAM 为一个有限状态自动机,接受且仅接受 \(S\) 的一个后缀。
同时,SAM 是这样的自动机中最小的那个,其中状态数至多为 \(2n - 1\),转移数至多为 \(3n - 4\)。
基本性质
- SAM 是一张 DAG。
- SAM 上从源点 \(t_0\) 出发经过的任意一条路径为原串的一个子串,因此 SAM 上一个节点对应一个子串集合。
单有这些基础性质是不够的,我们可以考虑多寻找一些 SAM 的性质来使用必要条件构造出 SAM。
首先我们需要引入一些强相关定义:
结束位置 endpos
对于字符串 \(s\) 的任意非空子串 \(t\),我们记 \(\mathrm{endpos}(t)\) 为 \(t\) 在 \(s\) 中的所有出现的结尾位置构成的集合。
对于 \(\rm endpos\),我们不加证明地给出如下性质。
对于 \(s\) 的任意两个非空子串 \(t_1, t_2(|t_1| \le |t_2|)\),其中 \(t_1\) 为 \(t_2\) 的一个后缀当且仅当:\(\mathrm{endpos}(t_2) \subseteq \mathrm{endpos}(t_1)\)。
对于 \(s\) 的任意两个非空子串 \(t_1, t_2(|t_1| \le |t_2|)\),一定满足:\(\mathrm{endpos}(t_1) \cap \mathrm{endpos}(t_2) = \mathrm{endpos}(t_2) / \varnothing\)。
在给出第三条性质之前,我们先定义 \(\rm endpos\) 集等价类,即我们将 \(\rm endpos\) 集相同的子串看作是一个等价类。
- 对于同一等价类内的子串,其长度连续。
有了关于 \(\rm endpos\) 集的性质之后,我们发现对于 SAM,需要满足如下性质:
- SAM 上任意一个节点对应子串的 \(\rm endpos\) 集相同。
对于任意的节点 \(x\),我们考虑其表示的任意一个子串 \(t\) 到经过其的后缀的结尾距离构成的集合 \(S\)。
不难发现就是从 \(x\) 开始往后遍历到所有终止态经过的节点数构成的集合。
该集合的构成显然与 \(t\) 无关,仅与 \(x\) 有关因此该性质成立。
- SAM 上不存在两个节点 \(u, v\) 使得 \(u, v\) 的 \(\rm endpos\) 集相同。
反证法:若存在则可以将两个节点合并(转移也合并),SAM 减小与最小性不符。
由此,我们可知 SAM 上的每个节点唯一对应原串的一个等价类。
后缀 Link
对于字符串 \(s\) 的任意一个子串 \(t\),定义后缀 \(\mathrm{link}_t\) 为 \(t\) 最长的后缀使得其与 \(t\) 的 \(\rm endpos\) 集不同,特别地,若不存在则将 \(\mathrm{link}_t \leftarrow 0\)。
容易得知,根据后缀 \(\rm link\) 连边,我们可以得到一颗树,这颗树称为 \(\rm parent\) 树。
但这个信息量是巨大的,我们不妨使用 SAM 的结构和 \(\rm endpos\) 的性质压缩这个结构。
- 根据 \(\rm endpos\) 集的定义,同一个等价类中子串的后缀 \(\rm link\) 是相同的。
因此我们可以将所有子串压缩为等价类一个节点,而将连边改为等价类之间的连边,从直观上来看,这样大大减少了连边数。
压缩之后为了储存信息,根据等价类的性质 \(3\),我们只需储存当前等价类内子串的最长长度 \(len\),和最短长度 \(minlen\) 即可。
同时,后缀 \(\rm link\) 还满足如下性质:
对于字符串 \(s\) 的任意子串 \(t_1\),对于另一个子串 \(t_2\),其中 \(t_2\) 为 \(t_1\) 在 \(\rm parent\) 树上的祖先当且仅当 \(t_2\) 为 \(t_1\) 的一个后缀。
\(\rm parent\) 树的节点数至多为 \(2n - 1\),即等价类 / SAM 上的节点数至多为 \(2n - 1\)。
考虑从上往下暴力构造一颗 \(\rm parent\) 树。
具体地,我们从根节点 \(t_0\) 出发,特别地,我们令此时的 \(\rm endpos\) 集为全集,同时维护一个字符串,初始时为空串。
每次我们在当前的的字符串前加上一个字符 \(c\),计算该字符串的 \(\rm endpos\) 集。
我们扣掉 \(\rm endpos\) 集为空集的字符串,若剩下的字符串 \(\rm endpos\) 集只存在一种,那么显然这些字符串与当前字符串的 \(\rm endpos\) 集相等,可以直接将这些字符串归为同一个等价类,此时节点数不变。
若剩下的字符串存在至少两种 \(\rm endpos\) 集,那么按照 \(\rm endpos\) 集的不同进行划分新建节点将这些节点连向当前节点。对这些新的节点递归操作。
我们只关心新建的节点总数量,可以发现这个流程可以抽象为将一个大小为 \(n\) 的集合不断划分至大小为 \(1\) 的集合,划分的次数即为新建的节点数量。
对于这个划分数量的最大值可以简单地得到一个 \(2n - 2\) 的上界,加上源点总共至多 \(2n - 1\) 个节点。
SAM 的构造
一般地,考虑增量法。令当前字符串为 \(s\),位置为 \(i\),需要往后加一个字符 \(c\)。
首先对于 \(s + c\) 的后缀,\(\rm endpos\) 集一定存在 \(i + 1\),且之前的所有 \(\rm endpos\) 都不存在 \(i + 1\),因此一定要新建节点 \(new\)。
为了求得 \(s + c\) 的所有后缀,我们可以遍历 \(s\) 的所有后缀然后在其后添加字符 \(c\)。
由此,我们可以借助 \(\rm parent\) 树来遍历 \(s\) 的所有后缀,同时,我们也需要维护出新建节点的后缀 \(\rm link\)。
此时我们发现,对于 \(s\) 的后缀,一定满足:
- 存在一个分界点使得前一部分后缀满足 \(+c\) 后在 \(s\) 中不出现,后一部分后缀满足 \(+c\) 后在 \(s\) 出现。同时该分界点在两个等价类之间。
因此对于分界点以前的节点,显然 \(+c\) 后 \(\rm endpos\) 集中只存在 \(i + 1\),因此可以直接在 SAM 上向 \(new\) 连边。
特别地,若不存在一个后缀满足 \(+c\) 后在 \(s\) 中出现,我们将 \(\mathrm{link}_{new} \leftarrow 0\),接下来将不会考虑这种情况。
我们找到第一个等价类使得其 \(+c\) 后在 \(s\) 中出现,令该节点为 \(p\),找到 \(+c\) 的串在原 \(s\) 当中的等价类 \(q\)。
此时我们有如下性质:
- \(len_q \ge len_p + 1, minlen_q \le minlen_p + 1\)。
我们可以发现,此时 \(q\) 这个等价类当中长度为 \((len_p + 1, len_q]\) 的子串 \(\rm endpos\) 集中将不存在 \(i + 1\),而长度为 \([minlen_q, len_p + 1]\) 的子串 \(\rm endpos\) 集中包含 \(i + 1\)。
(特别地,若 \(len_q = len_p + 1\) 我们最后考虑)
由此,我们需要将 \(q\) 分裂为两个等价类,我们新建节点 \(clo\),令它表示后者构成的等价类,那么需执行如下操作:
先将 \(q\) 的全部信息复制给 \(clo\),然后执行如下操作:len[clo] = len[p] + 1, link[q] = clo
由于等价类已经改变,因此之前连向 \(q\) 的转移需要重新定向。
显然需要重新定向的只有 \(p\) 的祖先节点当中有向 \(q\) 转移的节点,需要把它们都改为 \(clo\)。
同时,我们需要把 \(new\) 的后缀 \(\rm link\) 指向 \(clo\)。
最后,如果 \(len_q = len_p + 1\),那么 \(q\) 中所有的子串 \(\rm endpos\) 都会加上 \(i + 1\),我们只需要修改 \(new\) 的后缀 \(\rm link\) 指向 \(clo\)。
正确性证明
容易发现,除了最小性以外,开始提到的 SAM 需要满足的所有性质都能很容易地利用构造 SAM 的流程证明正确性。
而最小性的证明理论比较复杂,这里不涉及。
复杂度证明
状态数
在后缀 \(\rm link\) 中我们已经使用了一种与 SAM 构造无关的方式证明了一个上界为 \(2n - 1\)。
同时,通过 SAM 的构造,我们可以轻松地看出 SAM 的状态数也即等价类数量的一个上界为 \(2n - 1\)。
转移数
我们令 \(last\) 表示考虑完 \(s\) 构造的 SAM 中 \(s\) 这个整串对应的状态。
我们发现,每次向 \(new\) 连边,\(last\) 会一直跳后缀 \(link\),最后停下的位置为 \(top\) 那么其一定满足:
- \(minlen_{clo} \le minlen_{top} + 1\)。
令 \(top\) 由 \(lst\) 这个位置跳过来。
- \(minlen_{new} \le minlen_{lst} + 1\)
考虑令势能函数 \(\varphi(x) = minlen_x\)。
\]
于是总连边量(不包括复制连边)为:
\]
复制连边的复杂度证明好像假了,先鸽。。。先记着总转移数大小的结论:不超过 \(3n - 4\).
复杂度
首先,除了重定向部分,其余部分的复杂度与状态数和转移数相同,因此只考虑重定向部分的复杂度。
令势能函数 \(\lambda(x) = \mathrm{len}(\mathrm{link}(\mathrm{link}(x)))\),那么重定向的复杂度有上界:\(\varphi(top) - \lambda(new) \le \varphi(last) - \lambda(new)\).
更近一步地,将 \(last\) 初次跳两次后缀 \(\rm link\) 的复杂度单独写开:\(2 + \lambda(last) - \lambda(new)\) 那么有总复杂度:
\]
因此构建 SAM 的复杂度是线性的。
广义后缀自动机
定义
广义后缀自动机是一个 DFA 接受且仅接受给定的多个模式串 \(s_1, s_2, \cdots s_m\) 其中任意一个的任意后缀。
同样的,广义后缀自动机也满足最小性,核心构造思路(\(endpos\) 集等价类合并压缩)与 SAM 完全一致,性质为 SAM 扩展到多模式串层面,类似于 KMP 自动机和 AC 自动机之间的关系。
构造方式
首先类似于 AC 自动机的构建方式,我们将给定的 \(m\) 个模式串建出 trie 树。
然后按照 bfs 序依次建立,这时与 SAM 建立时满足的性质完全相同。
复杂度
状态数和转移数上界与 SAM 中相同,只与 trie 树大小相关。
重定向部分利用与 SAM 重定向部分一样的势能分析方法,复杂度也是线性的。
后缀自动机 (SAM)的更多相关文章
- [转]后缀自动机(SAM)
原文地址:http://blog.sina.com.cn/s/blog_8fcd775901019mi4.html 感觉自己看这个终于觉得能看懂了!也能感受到后缀自动机究竟是一种怎样进行的数据结构了. ...
- 【算法】后缀自动机(SAM) 初探
[自动机] 有限状态自动机的功能是识别字符串,自动机A能识别字符串S,就记为$A(S)$=true,否则$A(S)$=false. 自动机由$alpha$(字符集),$state$(状态集合),$in ...
- SPOJ 1811. Longest Common Substring (LCS,两个字符串的最长公共子串, 后缀自动机SAM)
1811. Longest Common Substring Problem code: LCS A string is finite sequence of characters over a no ...
- 后缀自动机SAM学习笔记
前言(2019.1.6) 已经是二周目了呢... 之前还是有一些东西没有理解到位 重新写一下吧 后缀自动机的一些基本概念 参考资料和例子 from hihocoder DZYO神仙翻译的神仙论文 简而 ...
- 浅谈后缀自动机SAM
一下是蒟蒻的个人想法,并不很严谨,仅供参考,如有缺误,敬请提出 参考资料: 陈立杰原版课件 litble 某大神 某大神 其实课件讲得最详实了 有限状态自动机 我们要学后缀自动机,我们先来了解一下自动 ...
- 后缀自动机(SAM)奶妈式教程
后缀自动机(SAM) 为了方便,我们做出如下约定: "后缀自动机" (Suffix Automaton) 在后文中简称为 SAM . 记 \(|S|\) 为字符串 \(S\) 的长 ...
- 【算法】后缀自动机(SAM) 例题
算法介绍见:http://www.cnblogs.com/Sakits/p/8232402.html 广义SAM资料:https://www.cnblogs.com/phile/p/4511571.h ...
- 后缀自动机(SAM)速成手册!
正好写这个博客和我的某个别的需求重合了...我就来讲一讲SAM啦qwq 后缀自动机,也就是SAM,是一种极其有用的处理字符串的数据结构,可以用于处理几乎任何有关于子串的问题,但以学起来异常困难著称(在 ...
- 【算法专题】后缀自动机SAM
后缀自动机是用于识别子串的自动机. 学习推荐:陈立杰讲稿,本文记录重点部分和感性理解(论文语言比较严格). 刷题推荐:[后缀自动机初探],题目都来自BZOJ. [Right集合] 后缀自动机真正优于后 ...
- 【文文殿下】对后缀自动机(SAM)的理解
后缀自动机,是一种数据结构,是由状态和转移关系构成的.它虽然叫做后缀自动机,可是他却与后缀并没有什么太大的联系. 后缀自动机的每一种状态都是原串的一些子串的集合,每个子串只唯一存在于某个状态中,对每一 ...
随机推荐
- Codeforces 888E:Maximum Subsequence(枚举,二分)
You are given an array a consisting of n integers, and additionally an integer m. You have to choose ...
- C++输出控制小数点后位数的方法
以C++输出小数点两位数为例 setprecision(n)使用方法总结 首先要记住写头文件 #include <iomanip> //不要忘了头文件 以下是控制小数位数的三种写法 //t ...
- Mysql溯源-任意文件读取👻
Mysql溯源-任意文件读取 前言 读了<MySQL蜜罐获取攻击者微信ID>的文章,文中说明了通过mysql蜜罐读取攻击者微信ID的过程,抱着学习的态度尝试了一下 原理 mysql中有一个 ...
- [opencv]白平衡算法中的灰度世界法,改善图像发红发蓝发绿的现象
#include<iostream> #include <opencv2/opencv.hpp> #include <math.h> using namespace ...
- Java EE数据持久化框架笔记 • 【目录】
章节 内容 实践练习 Java EE数据持久化框架作业目录(作业笔记) 第1章 Java EE数据持久化框架笔记 • [第1章 MyBatis入门] 第2章 Java EE数据持久化框架笔记 • [第 ...
- Java常用的开发库推荐
我是3y,一年CRUD经验用十年的markdown程序员 今天来讲讲来给大家聊聊开发提速的东西了:工具包.Lombok和常用库 01.什么是工具包 基本上,每个项目里都有一个包,叫做utils.这个 ...
- lscpu
[root@kvm02 ~]# lscpu Architecture: x86_64 #cpu架构CPU op-mode(s): 32-bit, 64-bitByte Order: Littl ...
- python 使用demical模块四舍五入
前言: 断言部分需要用到四舍五入,首先使用的是python自带的round(),做四舍五入进位处理,但使用过程中,发现round似乎是当保留小数位的最后一位是偶数,和保留小数位后一位为5时,就不进位. ...
- unittest_TestSuite测试套件(2)
在前面一章中演示了unittest如何执行一个简单的测试,但有两个问题: 我们知道测试用例的执行顺序是根据测试用例名称顺序执行的,在不改变用例名称的情况下,我们怎么来控制用例执行的顺序呢? 一个测试文 ...
- 在quasar 注册全局filter
A common use case for Quasar applications is to run code before the root Vue app instance is instant ...