某神犇:“初三还不会后缀自动机,那就退役吧!”

听到这句话后,我的内心是崩溃的。

我还年轻,我还不想退役……于是,我在后来,努力地学习后缀自动机。

终于,赶在初三开学前,我终于学会了后缀自动机。

好开心~~~


后缀自动机是一个很奇妙的东西。这是一个处理字符串的神器。如果可以灵活运用,就会有无限的奥妙。

这个东西,说真的,非常不好懂。网上的各种资料,想找到属于自己的,是一件非常难的事情。好不容易地,我找到几个资料,在这里分享一下。

另外,在学后缀自动机的时候,我充分地领悟了一个道理——千言万语不如一标。后缀自动机这种东西,稍微了解概念后,仔细看看标程,那么就会很快的理解做法。至于理解它是为什么……打多了就有感觉了。我的理解就有些感性、模糊,但我一定要好好弄清楚,这样才能将后缀自动机发扬光大。

还有,吐槽一点。周围的大神太多了,有的在几个月前已经学了后缀自动机,我对他们的智商感到羡慕不已,甚至是没学后缀数组就会后缀自动机了。看着他们手推后缀自动机时,我有一种很无奈的感觉,当初同一起点的我们已经相差越来越远,遥不可及……


参考资料

从最长公共子串到后缀自动机(LCS->SAM)

后缀自动机构造过程演示

后缀自动机学习总结


后缀自动机长啥样

假如我们将字符串的所有后缀加入一个Trie中,就会是下面这个玩意儿(以aabbab为例)



这么大棵树,当字符串很大时,一定建不成这棵树。

我们可以发现,其实这些后缀是有很多相似之处的。

那么怎么办?

当然是,压!

怎么压?

有个叫后缀树的东西,但是,这不是今天的重点。

我们将它压起来,将它压成一个有向无环图。



你可以发现,在后缀自动机上,从起点开始跑,每个路径都能够表示不同的子串

上面的那棵Trie上的每一个节点所表示的字符串(即原字符串的子串)都可以在后缀自动机上表示出来。

这个后缀自动机是能压到什么程度?

我可以告诉你,它的时空复杂度是O(n)" role="presentation" style="position: relative;">O(n)O(n)的。

一些概念

节点所代表的含义

后缀自动机中,每一个节点表示子串的right" role="presentation" style="position: relative;">rightright集合相同的子串集合

啊哈?啥意思?

一个子串的right" role="presentation" style="position: relative;">rightright集合,表示的是这个子串在字符串中结束位置集合

而SAM上的每个节点,表示的是right" role="presentation" style="position: relative;">rightright集合相同的子串集合。

思考一下这个集合的性质。

很显然,因为所有结束位置相同,对于这个集合中任意两个子串,一个子串必定是另一个的后缀

比如说,abcabcabc

那么子串,abcbcc在同一个集合之中。

同时我们发现,这个集合中的所有的子串,都是集合中最大的子串的后缀,并且这些后缀是连续的。也就是说,如果abcc在同一集合,bc不可能不在这个集合。

那么每个节点所代表的子串有个长度的范围,记为minleni" role="presentation" style="position: relative;">minleniminleni和maxleni" role="presentation" style="position: relative;">maxlenimaxleni。

对于字符串abcabcabcc(注意和上面的例子不同),abcbc在同一集合中,则范围是[2,3]" role="presentation" style="position: relative;">[2,3][2,3]。

还有,两个不同的节点,它们表示的子串集合不相交,而且right" role="presentation" style="position: relative;">rightright集要么一个真包含另一个,要么不相交。

fail指针

既然是自动机,那怎么可能没有fail" role="presentation" style="position: relative;">failfail指针呢?

对于一个节点,它的fail" role="presentation" style="position: relative;">failfail指针只有一个(废话!)。

fail" role="presentation" style="position: relative;">failfail指针指向的节点的right" role="presentation" style="position: relative;">rightright集合真包含当前节点的right" role="presentation" style="position: relative;">rightright集合。而且fail" role="presentation" style="position: relative;">failfail指针指向的节点的集合的maxlen" role="presentation" style="position: relative;">maxlenmaxlen最大

再思考一下有哪些性质。

right" role="presentation" style="position: relative;">rightright集合真包含当前节点的right" role="presentation" style="position: relative;">rightright集合,那么fail" role="presentation" style="position: relative;">failfail指向节点表示的字符串集必定都是当前节点的字符串集中任意一个字符串的后缀。

又因为maxlen" role="presentation" style="position: relative;">maxlenmaxlen最大,所以当前节点的最短后缀和它的最长后缀一定是连续的。也就是说,failmaxlen+1=thisminlen" role="presentation" style="position: relative;">failmaxlen+1=thisminlenfailmaxlen+1=thisminlen。所以minlen" role="presentation" style="position: relative;">minlenminlen是不需要记录的。

对于字符串abcabcabcc,表示abcbc的节点的fail" role="presentation" style="position: relative;">failfail指针指向的是表示c的节点。

因为right" role="presentation" style="position: relative;">rightright集合真包含,所以,fail" role="presentation" style="position: relative;">failfail指针形成了一棵树,称为fail" role="presentation" style="position: relative;">failfail树



上图是aabbab的后缀自动机,黑色的是转移边,蓝色的是fail" role="presentation" style="position: relative;">failfail边。

这幅图来自从最长公共子串到后缀自动机(LCS->SAM),在此再次感谢这个博主。不过为什么有水印……

后缀自动机的构建

增量法,即每一次插入一个字符。

先放标程,千言万语不如一标。

SAM的标程贼短,而且实用。比SA还好打。

#define MAXN 100000
struct Node{
int len;
Node *c[26];
Node *fail;
} d[MAXN*2+10];
Node *null,*last,*S;
int cnt;
void sam_init(){
null=&d[0];
++cnt;
d[cnt].len=0;
d[cnt].fail=null;
S=last=&d[cnt];
}
void sam_insert(int ch){
Node *now=&d[++cnt];
now->len=last->len+1;
Node *p;
for (p=last;p && !p->c[ch];p=p->fail)
p->c[ch]=now;
if (!p)
now->fail=S;
else{
Node *q=p->c[ch];
if (p->len+1==q->len)
now->fail=q;
else{
Node *clone=&d[++cnt];
clone->len=p->len+1;
memcpy(clone->c,q->c,sizeof q->c);
clone->fail=q->fail;
for (;p && p->c[ch]==q;p=p->fail)
p->c[ch]=clone;
now->fail=q->fail=clone;
}
}
last=now;
}

再描述一下这个过程。

1. 新建一个节点,设这个节点为u" role="presentation" style="position: relative;">uu。

2. 从last" role="presentation" style="position: relative;">lastlast(最后一个点)开始,沿着fail" role="presentation" style="position: relative;">failfail边跳。如果没有ch" role="presentation" style="position: relative;">chch的转移边,则向u" role="presentation" style="position: relative;">uu连上一条边。否则退出。

3. 如果一直未找到ch" role="presentation" style="position: relative;">chch的转移边,那么fail" role="presentation" style="position: relative;">failfail指向初始点S" role="presentation" style="position: relative;">SS。否则进入4。

4. 设这个点为p" role="presentation" style="position: relative;">pp,转移边连向的点为q" role="presentation" style="position: relative;">qq。如果pmaxlen+1=qmaxlen" role="presentation" style="position: relative;">pmaxlen+1=qmaxlenpmaxlen+1=qmaxlen,就直接将u" role="presentation" style="position: relative;">uu的fail" role="presentation" style="position: relative;">failfail指向q" role="presentation" style="position: relative;">qq。否则进入5。

5. 新建一个节点clone" role="presentation" style="position: relative;">cloneclone,clonemaxlen=pmaxlen+1" role="presentation" style="position: relative;">clonemaxlen=pmaxlen+1clonemaxlen=pmaxlen+1,clone" role="presentation" style="position: relative;">cloneclone连向q" role="presentation" style="position: relative;">qq连向的所有节点,并且clone" role="presentation" style="position: relative;">cloneclone的fail" role="presentation" style="position: relative;">failfail指针指向p" role="presentation" style="position: relative;">pp,进入6。

6. p" role="presentation" style="position: relative;">pp继续沿着fail" role="presentation" style="position: relative;">failfail跳,所有指向q" role="presentation" style="position: relative;">qq的指针全都指向clone" role="presentation" style="position: relative;">cloneclone。最后将u" role="presentation" style="position: relative;">uu和q" role="presentation" style="position: relative;">qq的fail" role="presentation" style="position: relative;">failfail指向clone" role="presentation" style="position: relative;">cloneclone。

7. 最后,更新last" role="presentation" style="position: relative;">lastlast。

原理?

首先,last" role="presentation" style="position: relative;">lastlast是先前最后的节点,而且maxlen" role="presentation" style="position: relative;">maxlenmaxlen是最长的。在fail" role="presentation" style="position: relative;">failfail树上,last" role="presentation" style="position: relative;">lastlast到S" role="presentation" style="position: relative;">SS的链上所有节点的集合,是前面的字符串的所有后缀。

所以,非常自然的,用链上的节点来连向新的节点。

上面的步骤中,主要讲一下步骤4~6。其它的比较易懂。

如果pmaxlen+1=qmaxlen" role="presentation" style="position: relative;">pmaxlen+1=qmaxlenpmaxlen+1=qmaxlen,那么q" role="presentation" style="position: relative;">qq所代表的子串只有一个,所以u" role="presentation" style="position: relative;">uu的fail" role="presentation" style="position: relative;">failfail指向q" role="presentation" style="position: relative;">qq理所当然。

否则,新开一个节点,强行clonemaxlen=pmaxlen+1" role="presentation" style="position: relative;">clonemaxlen=pmaxlen+1clonemaxlen=pmaxlen+1,然后替代q" role="presentation" style="position: relative;">qq的位置(当然,不意味着q" role="presentation" style="position: relative;">qq没了)。让u" role="presentation" style="position: relative;">uu和q" role="presentation" style="position: relative;">qq都指向它。

我怎么好像是在念过程啊……好吧,这一段我理解得比较感性,朦胧,不好描述。最好看上面的参考资料。

复杂度?

每次加入一个字符最多增加两个节点,所以节点个数是线性的。

边是线性的,整个建SAM的过程都是线性的……(我当然不会证)。

反正时空复杂度O(n)" role="presentation" style="position: relative;">O(n)O(n)。

记住就好。

还有代码复杂度,简单易打,是吧……


例题

现在我打出了三道例题。

一个是洛谷上的模板。

一个是KMP裸题。

还有一个是求不同子串的第k" role="presentation" style="position: relative;">kk大。

这些都是简单的模板题。

这篇博客有很多好例题,只不过我初学,要有个慢热的过程。以后有时间就做

后缀自动机SAM的更多相关文章

  1. [转]后缀自动机(SAM)

    原文地址:http://blog.sina.com.cn/s/blog_8fcd775901019mi4.html 感觉自己看这个终于觉得能看懂了!也能感受到后缀自动机究竟是一种怎样进行的数据结构了. ...

  2. 【算法】后缀自动机(SAM) 初探

    [自动机] 有限状态自动机的功能是识别字符串,自动机A能识别字符串S,就记为$A(S)$=true,否则$A(S)$=false. 自动机由$alpha$(字符集),$state$(状态集合),$in ...

  3. SPOJ 1811. Longest Common Substring (LCS,两个字符串的最长公共子串, 后缀自动机SAM)

    1811. Longest Common Substring Problem code: LCS A string is finite sequence of characters over a no ...

  4. 后缀自动机SAM学习笔记

    前言(2019.1.6) 已经是二周目了呢... 之前还是有一些东西没有理解到位 重新写一下吧 后缀自动机的一些基本概念 参考资料和例子 from hihocoder DZYO神仙翻译的神仙论文 简而 ...

  5. 浅谈后缀自动机SAM

    一下是蒟蒻的个人想法,并不很严谨,仅供参考,如有缺误,敬请提出 参考资料: 陈立杰原版课件 litble 某大神 某大神 其实课件讲得最详实了 有限状态自动机 我们要学后缀自动机,我们先来了解一下自动 ...

  6. 后缀自动机(SAM)奶妈式教程

    后缀自动机(SAM) 为了方便,我们做出如下约定: "后缀自动机" (Suffix Automaton) 在后文中简称为 SAM . 记 \(|S|\) 为字符串 \(S\) 的长 ...

  7. 【算法】后缀自动机(SAM) 例题

    算法介绍见:http://www.cnblogs.com/Sakits/p/8232402.html 广义SAM资料:https://www.cnblogs.com/phile/p/4511571.h ...

  8. 后缀自动机(SAM)速成手册!

    正好写这个博客和我的某个别的需求重合了...我就来讲一讲SAM啦qwq 后缀自动机,也就是SAM,是一种极其有用的处理字符串的数据结构,可以用于处理几乎任何有关于子串的问题,但以学起来异常困难著称(在 ...

  9. 【算法专题】后缀自动机SAM

    后缀自动机是用于识别子串的自动机. 学习推荐:陈立杰讲稿,本文记录重点部分和感性理解(论文语言比较严格). 刷题推荐:[后缀自动机初探],题目都来自BZOJ. [Right集合] 后缀自动机真正优于后 ...

  10. 【文文殿下】对后缀自动机(SAM)的理解

    后缀自动机,是一种数据结构,是由状态和转移关系构成的.它虽然叫做后缀自动机,可是他却与后缀并没有什么太大的联系. 后缀自动机的每一种状态都是原串的一些子串的集合,每个子串只唯一存在于某个状态中,对每一 ...

随机推荐

  1. leetcode-241-为运算表达式设置优先级*

    题目描述: 方法:分治* class Solution: def diffWaysToCompute(self, input: str) -> List[int]: if input.isdig ...

  2. JavaScript开发人员必知的10个关键习惯

    还在一味没有目的的编写JavaScript代码吗?那么你就OUT了!让我们一起来看看小编为大家搜罗的JavaScript开发人员应该具备的十大关键习惯吧! 随着新技术的不断发展,JavaScript已 ...

  3. 4_4.springboot之Web开发登录和拦截器

    1.登录处理 1).禁用模板引擎的缓存 # 禁用缓存 spring.thymeleaf.cache=false 2).页面修改完用ctrl+f9:重新编译: LoginController @Cont ...

  4. assert(断言)

    Python assert(断言)用于判断一个表达式,在表达式条件为 false 的时候触发异常. 语法格式: assert expression 等价于: if not expression: ra ...

  5. js new运算符

    用代码模拟这个逻辑就是

  6. spring:AOP面向切面编程(注解)03

    使用注解写aop时最好使用环绕通知写 切面类: /** * 用于记录日志的工具类,它里面提供了公共的代码 */ @Component("logger") @Aspect //表示当 ...

  7. BP 算法之一种直观的解释

    0. 前言 之前上模式识别课程的时候,老师也讲过 MLP 的 BP 算法, 但是 ppt 过得太快,只有一个大概印象.后来课下自己也尝试看了一下 stanford deep learning 的 wi ...

  8. [Luogu2135] 方块消除【区间Dp】

    Online Judge:P2135 方块消除(这题不用预处理) Label:区间Dp 题目描述 Jimmy最近迷上了一款叫做方块消除的游戏.游戏规则如下:n个带颜色方格排成一列,相同颜色的方块连成一 ...

  9. linux 解压 WinRAR 压缩文件

    1.Download rar for linux wget http://www.rarlab.com/rar/rarlinux-x64-5.5.b1.tar.gz 2.Configure rar t ...

  10. webpack静态资源拷贝插件

    处理不需要使用webpack统一打包处理或webpack不支持的文件 安装 npm install copy-webpack-plugin --save-dev 配置 const copyWebpac ...