部分改编自OI WIKI

先从一个简单的问题入手:

给定一个串,构造一个图,使其能够表示它的所有子串。

显然一个子串就是一个后缀的前缀。所以一个很显然的方式就是把所有后缀扔进trie里。

比如当前串是aaba。

但是我们发现,这样是不是有点浪费?比如"ba"这个字符串好像在里面出现了三遍。

既然树型结构已经不能更优了,那么我们不妨另辟蹊径,考虑能不能找到一个算法,用一个DAG代替这个trie树。比如将上述三个红色圆圈合并,可以变成:

答案是肯定的。这个算法就是SAM。

先在这里展示一些简单的字符串的后缀自动机。

这里用蓝色表示初始状态,用绿色表示终止状态。

对于字符串 \(s=\varnothing\) :

对于字符串 \(s=\texttt{a}\):

对于字符串 \(s=\texttt{aa}\):

对于字符串 \(s=\texttt{ab}\):

对于字符串 \(s=\texttt{abb}\):

对于字符串 \(s=\texttt{abbb}\):


看起来有点复杂?那么我们该如何建这样一个后缀自动机呢?

这里先给出结论:我们可以在 \(O(n)\) 时间内建出这样的后缀自动机而且代码极短。

在建后缀自动机之前,我们需要先了解一下一些性质:

我们定义 \(\operatorname{endpos}(t)\) 为母字符串 \(s\) 的非空子串 \(t\) 在字符串 \(s\) 中的所有结束位置的集合。

例如 s="\(\texttt{aabab}\)",t="\(\texttt{ab}\)",那么 \(\operatorname{endpos}(t)=\{2,4\}\)。

关于endpos集合的性质 OI WIKI 讲的很详细。但是有点繁琐。这里给出我的理解:

根据endpos集合我们可以一个树形关系,其中根结点为 \(\varnothing\),每个节点的endpos集合都完全包含其儿子的endpos集合。且对于两个没有祖先关系的点,endpos交为 \(\varnothing\)。

事实上这个树形关系就是parent树。这在后面会用到。

关于证明详见 OI WIKI

可以发现,对于SAM的某个节点 \(u\),其表示的字符串是 \(s\) 长度为 \(len_u\) 的前缀中长度大于某一长度的后缀。而 \(len_u\) 显然也是 \(u\) 表示的子串中最长的那个。

可以证明,\(len_u\) 等于它插入时的字符串的长度。

我们考虑记 \(\operatorname{link}(u)=v\) 为最长的 \(len_v\) 使 \(\operatorname{endpos}(u)\subseteq\operatorname{endpos}(v)\)。

那么 \(u\) 表示的字符串是 \(s\) 长度为 \(len_u\) 的前缀中长度大于 \(len_{\operatorname{link}(u)}\) 的后缀。

证明详见 OI WIKI

例如当s="\(\texttt{abcbc}\)"时,SAM和parent树如下所示:


接下来我们可以开始建SAM了。

先放一个板子:

void insert(int c)
{
int p=las,q=las=++cnt;
len[q]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=q;
if(!p) fa[q]=1;
else
{
int np=ch[p][c];
if(len[np]==len[p]+1) fa[q]=np;
else
{
int nq=++cnt;
memcpy(ch[nq],ch[np],sizeof(ch[nq]));
fa[nq]=fa[np];
fa[np]=fa[q]=nq;
len[nq]=len[p]+1;
for(;p && ch[p][c]==np;p=fa[p]) ch[p][c]=nq;
}
}
}

首先需要注意一点,这里的 \(\text{fa}\) 本质上其实是 \(\text{link}\),即并不存在fa[ch[u][c]]=u之类的关系。

我们一步步解释这段程序在干什么。

    int p=las,q=las=++cnt;
len[q]=len[p]+1;
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=q;

\(las\) 是我们上一次插入的节点,而 \(q\) 是我们要加入的节点。

如果我们记 \(s_u\) 表示节点 \(u\) 能表示的最长的子串,那么 \(s_q=s_{p}+c\)。

所以显然有 len[q]=len[p]+1。接下来我们要给 \(q\) 找 \(fa\)。

接下来个人感觉特别像KMP,即我们知道 \(s_{fa[p]}\) 一定是 \(s_p\) 的一个后缀。我们希望找到一个已经存在的状态,它存在一个 \(c\) 边的扩展。

如果不存在当前 \(p\) 不存在 \(c\) 边的扩展,那么显然 \(q\) 本身就是一个新的扩展,直接赋值 ch[p][c]=q,然后继续向上找。

    if(!p) fa[q]=1;
else
{
int np=ch[p][c];
if(len[np]==len[p]+1) fa[q]=np;

如果找到根了还是没有找到 \(c\) 边的扩展,说明当前串不存在前驱,那么我们直接令 fa[q]=1

否则我们找到了一个 \(c\) 边的扩展,显然这是存在 \(c\) 边的扩展中最长的子串。接下来判断这个扩展是不是直接的,即是 \(s_{np}=s_p+c\) 还是 \(s_{np}=s_p+c\dots\)。

如果是前者,那么显然 \(np\) 就是 \(q\) 的前驱,即 \(\operatorname{endpos}(q)\subseteq\operatorname{endpos}(np)\),直接赋值即可。

否则比较麻烦,即 \(np\) 表示的某个串是 \(q\) 的前驱。

        else
{
int nq=++cnt;
memcpy(ch[nq],ch[np],sizeof(ch[nq]));
fa[nq]=fa[np];
fa[np]=fa[q]=nq;
len[nq]=len[p]+1;
for(;p && ch[p][c]==np;p=fa[p]) ch[p][c]=nq;
}

对于这种情况,我们需要分裂出一个点 \(nq\),令这个点 \(s_{nq}=s_p+c\) 。

可以发现由于 \(nq\) 是 \(np\) 的一个子集,\(np\) 的所有转移边 \(nq\) 也都有。

那么 fa[nq]=fa[np]fa[np]=fa[q]=nq 也就很显然了。

接下来,\(nq\) 代替了 \(np\) 成为了 \(len\) 更短的点,那么显然所有连向了 \(np\) 的边都连向了 \(nq\)。

这样我们就建完了一棵SAM。

显然这样空间复杂度 \(O(n)\),因为我们插入一个字符最多会增加2个点。关于时间复杂度的证明详见OI WIKI


接下来我们看一些经典套路:

1.求本质不同子串个数

首先对字符串 \(S\) 构造后缀自动机。

每个 \(S\) 的子串都相当于自动机中从根开始的路径。因此不同子串的个数等于自动机中以1为起点的不同路径的条数。

考虑到SAM是一个DAG,不同路径的条数可以通过动态规划计算。当然最后还要去掉空串。

时间复杂度 \(O(|S|)\) 。

当然这种方法有很大局限性,因为每一次都需要dfs一遍。所以大多数时候采用的都是下述方法:

我们知道一个字符串唯一对应一个节点,而一个节点表示的是长度为 \(len_u\) 的前缀中长度在 \((len_{fa},len_u]\) 的后缀。

所以显然有 \(\text{答案}=\sum{(len_{fa_u}-len_u)}\)。

2.字典序第 \(k\) 大子串

可以发现一个SAM是一个DAG,所以我们可以dp求出从一个点出发的子串数量。

那么很明显,我们从 'a'->'z' 依次查看,找到第一个满足子串数量前缀和 \(\geq k\) 的转移边,转移即可。

时间复杂度 \(O(|S|)\)。

3.动态求出现次数

给定文本串 \(S\) 和若干询问串 \(T_i\),动态询问 \(T_i\) 在 \(S\) 中出现次数。

我们知道,SAM本质上是将一个trie压缩后得到的结果,所以我们可以直接跳转移边得到。

最后得到的结果的状态数就是答案。状态数可以dfs预处理出来。

时间复杂度 \(O(|S|+|T|)\)。

4. 求两个前缀的LCS

这本来是归SA管的一道板子题,但偏偏有些出题人想要动态加字符。

可以发现,parent树本质是一棵压缩的trie树,保留其中的关键节点。

如果这是trie树,显然我们可以直接找trie上的LCA。而parent树保留的就是一棵虚树,所以显然也符合条件。

\(\color{black}{\text{O}}\color{red}{\text{IerWanHong}}\) \(\texttt{orz}\)。

如何建一个SAM的更多相关文章

  1. “为什么DirectX里表示三维坐标要建一个4*4的矩阵?”

    0x00 前言 首先要说明的是,本文的标题事实上来自于知乎上的一个同名问题:为什么directX里表示三维坐标要建一个4*4的矩阵? - 编程 .因此,正如Milo Yip大神所说的这个标题事实上是存 ...

  2. 每建一个Activity都要注册权限Manifest.xml

    每建一个Activity都要注册权限Manifest.xml 但是有时候自动注册好了,注意!不然的话是不能调用的!!!!!<activity android:name=".MainVi ...

  3. 如何使用maven建一个web3.0的项目

    使用eclipse手动建一个maven的web project可能会有版本不合适的情况,例如使用spring的websocket需要web3.0什么的,不全面的修改可能会出现各种红叉,甚是苦恼.我从我 ...

  4. 基于gulp编写的一个简单实用的前端开发环境好了,安装完Gulp后,接下来是你大展身手的时候了,在你自己的电脑上面随便哪个地方建一个目录,打开命令行,然后进入创建好的目录里面,开始撸代码,关于生成的json文件请点击这里https://docs.npmjs.com/files/package.json,打开的速度看你的网速了注意:以下是为了演示 ,我建的一个目录结构,你自己可以根据项目需求自己建目

    自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...

  5. JGUI源码:从头开始,建一个自己的UI框架(1)

    开篇 1.JGUI是为了逼迫自己研究底层点的前端技术而做的框架,之前对web底层实现一直没有深入研究,有了技术瓶颈,痛定思痛从头研究, 2.虽然现在vue技术比较火,但还在发展阶段,暂时先使用JQue ...

  6. 学习建一个spring-Mvc项目

    学习建一个spring-Mvc项目 首先要有jdk1.8以上,spring,mybatis,以及整合jar包,tomcat ,然后配置环境(前面有配置得方法). 1)右键new project,--& ...

  7. 建一个maven项目

     建一个普通的maven项目(eclipse) 需要的jar和文件: eclipse :jdk1.8.0_144 maven:apache-maven-3.5.3     进入(下载):http:// ...

  8. 转载 STM32 使用Cubemx 建一个USB(HID)设备下位机,实现数据收发

    STM32 使用Cubemx 建一个USB(HID)设备下位机,实现数据收发  本文转载自 https://www.cnblogs.com/xingboy/p/9913963.html 这里我主要说一 ...

  9. 如何建一个Liferay 7的theme

    首先附上原文链接Creating theme and Deploying in liferay 7 by using Eclipse 1.第一步:建一个Liferay module 项目,选择them ...

随机推荐

  1. binary hacks读数笔记(readelf基本命令)

    一.首先对readelf常用的参数进行简单说明: readelf命令是Linux下的分析ELF文件的命令,这个命令在分析ELF文件格式时非常有用,下面以ELF格式可执行文件test为例详细介绍: 1. ...

  2. exec系列函数详解

    execve替换进程映像(加载程序):execve系统调用,意味着代码段.数据段.堆栈段和PCB全部被替换.在UNIX中采用一种独特的方法,它将进程创建与加载一个新进程映像分离.这样的好处是有更多的余 ...

  3. MySQL全面瓦解12:连接查询的原理和应用

    概述 MySQL最强大的功能之一就是能在数据检索的执行中连接(join)表.大部分的单表数据查询并不能满足我们的需求,这时候我们就需要连接一个或者多个表,并通过一些条件过滤筛选出我们需要的数据. 了解 ...

  4. EDI在服装行业的应用

    EDI发展迅速,从最初应用于汽车.物流.零售等行业开始,应用范围不断扩大.当下金融行业.服装行业也加入到使用EDI进行数据传输的队伍中.本文主要介绍服装行业通过EDI系统实现业务数据收发,本次EDI项 ...

  5. Python_selenium_WebDriver API,ActionChains鼠标, Keys 类键盘

    WebDriver 提供的八种定位方法: find_element_by_id() find_element_by_name() find_element_by_class_name() find_e ...

  6. 分布式 task_master / task_worker

    17:08:0317:08:04 在Thread(线程)和Process(进程)中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分 ...

  7. API的使用(3)Arrays 类,Math类,三大特性--继承

    Arrays类 概述   java.util.Arrays此时主要是用来操作数组,里面提供了很多的操作API的方法.如[排序]和[搜索]功能.其所有的方法均为静态方法,调用起来非常简单. 操作数组的方 ...

  8. 理解 ASP.NET Core: 处理管道

    理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...

  9. 面试必看!靠着这份字节和腾讯的面经,我成功拿下了offer!

    准备 敲定了方向和目标后就开始系统准备,主要分为以下几个方面来准备. 算法题 事先已经看过别人的社招面经知道头条每轮技术面都有算法题,而这一块平时练习的比较少,校招时刷的题也忘记了很多.因此系统复习的 ...

  10. FL Studio中如何制作和混音警报声

    警报声在当今的许多电影配乐中,或者电子音乐的环境fx中经常出现.为了使用这种尖刺的警示声音,我们除了自己录制已有的警报声以外,也可以使用FL Studio20中的合成器和混音插件来制作属于自己的警报声 ...