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

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

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

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

好开心~~~


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

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

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

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


参考资料

从最长公共子串到后缀自动机(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. iOS开发NSLayoutConstraint代码自动布局

    1.NSLayoutConstraint简介 适配界面大多用Masonry工具,也是基于NSLayoutConstraint写的!通过使用两个类方法实现自动布局: + (NSArray<__ki ...

  2. python paramiko模块学习分享

    python paramiko模块学习分享 paramiko是用python语言写的一个模块,遵循SSH2协议,支持以加密和认证的方式,进行远程服务器的连接.paramiko支持Linux, Sola ...

  3. VUE的组件为什么要EXPORT DEFAULT 转载

    Vue的组件为什么要export default   Vue 的模块机制 Vue 是通过 webpack 实现的模块化,因此可以使用 import 来引入模块,例如: 此外,你还可以在 bulid/w ...

  4. Astyle 快速入门,常用指令

    --style=java -n -p -c !E astyle是一个命令行工具,命令语法很简单:          astyle [options] < original > Beauti ...

  5. <linux常用命令>初级版

    显示时间 date 显示日历cal 变换目录 cd 显示当前所在目录 pwd 建立新目录 mkdir -p a/b/c 删除空目录 rmdir 当前目录下文件和目录显示 ls 复制 cp 文件 路径 ...

  6. mysql初次使用

  7. netty 私有协议栈

    通信协议从广义上区分,可以分为公有协议和私有协议.由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好.绝大多数的私有协议传输层都基于TCP/I ...

  8. READING | 我是一只IT小小鸟

    “世界是如此的熙熙攘攘,让年轻的心找不到方向,但这些人是不能小看的啊,如果他们开始敲打自己的命令行.” “知之者不如好知者,好之者不如乐之者”,很多IT界的优秀人才都对计算机技术或者IT技术有着浓厚的 ...

  9. Linux vi和vim编辑器(1)

    1:vi和vim的三种常见模式  1.1正常模式 在正常模式下,我们可以使用快捷键: 以vim打开一个档案就直接进入一般模式了(这是默认的模式).在这个模式中,你可以使用[上下左右」按键来移动光标,你 ...

  10. selenium学习笔记——driver.get(url) 页面加载时间太长

    # 两个同时设置才行 # 实现效果:加载状态停止,进行代码下一步操作 driver.set_page_load_timeout(10) driver.set_script_timeout(10) # ...