SAM学习笔记

后缀自动机(模板)NSUBSTR(Caioj1471 || SPOJ 8222

  【题意】

    给出一个字符串S(S<=250000),令F(x)表示S的所有长度为x的子串中,出现次数的最大值。求F(1)..F(Lengh(S));

  【输入格式】

    一个字符串

  【输出格式】

    依次输出答案

  【样例输入】

    ababa

  【样例输出】

    3

    2

    2

    1

    1


【算法分析】

   相信大家在学习这个专题时已经接触过很多有关解决字符串问题的其它算法了。但是我们接下来要学的这个专题对于解决字符串的一系列难题都是一件利器----SAM后缀自动机(单词的有向无环图)。

  为啥要学习这个算法呢?因为使用后缀自动机来解决字符串的问题很多情况下都可以在线性时间内解决。也就是O(n)差不多啦~(关于具体证明可以参考国家集训队陈立杰的后缀自动机论文)

后缀自动机的基本概念:

    能够接收一个串S的所有后缀,可以包含所有S子串的信息,并且它的状态数最少的有向无环图。如果从初始状态root由任意路径走到一个状态(点),按顺序写出所有经过的字符,最后得出来的必然是原串的某一子串;如果走到了终止状态,那么这时所形成的必然是后缀。(建成如下)

再简单粗暴的概括一下大致做法:

    假设当前建好了S的其中一个前缀的后缀自动机A,再添加一个字符x,那么就应该得到S的另一个前缀Ax的后缀自动机。像这样按顺序一个一个插入,最后把所有的都建好了之后,就得到了字符串S的后缀自动机。

    那么回过头来看我们就很容易发现,这个构造过程其实是在线的,可以在任意时刻询问当前S串的信息,也可以再次在末尾插入字符。(感觉好像树结构一样)不过删除的操作在这里时不支持的。

基础数据结构:

    直接上重点:right集合。这个东西在自动机构造里面并没有很明显的使用,但是right集合的中心思想也正是构造SAM的核心思路。

    对于图中任意的一个点都可以(因为询问所经过的路径不同)表示成原串的一个或多个子串, 那么这个点的right集合就可以表示为由点为末尾组成的字符串后在原串中右端点(末尾)的位置。那么right集合的重要性主要体现在构造时对这个核心思路的使用;次要的作用就是,我们可以通过right集合的大小,求出某个子串在原串中出现的次数(对于给出的模板题就大有用处了)。

    last:上一个建立的节点。

    deep[ ]:询问到某个节点所能够经过的最大长度。

    ch[ ].son[ ]:(son[i]表示一个字符)表示此节点是否能联通某个字符。

    fail[ ]:这个数组也至关重要。先将定义,fail[i]表示i这个节点上一次出现的位置,也就是当前right集合位置的前一个。返回的节点也是上一个可以接收新节点的节点(如果当前节点可以接收成功,那么所返回的节点一定也可以)。

具体做法:

    为了让大家更好的理解接下来的构造方法,这里给大家疏通一些主要性质。

    1、从初始状态(用root表示)往后询问,到任意节点p的所有路经上所组成的字符串,都是子串之一。(根据这个性质,还可以推出性质2)

    2、如果到节点p可以成为新后缀,那么从root到任意节点p(要注意和p相同的节点的存在)的每条路经所组成的所有字符串,都是后缀。

    3、如果当前字符结点p可以接收新字符成为后缀,那么p的fail指向的结点也可以接收后缀,反过来就不行。

    下面讲正式做法:

    现在要插入字符x,也就是把A的后缀自动机变为Ax的后缀自动机。我们把要插入的节点储存为np,找到last,让last不断找fail(直到有x儿子或root才停止寻找)。假设当前fail找到了p节点(还没有停止),如果这个p没有x儿子,那么我们就把x的儿子赋值为np。停止寻找后,我们就要处理当前跳到的有x儿子的p(这个p是最终的)了。

    处理的时候会出现两种情况。(假设p的x儿子是q节点)

    1、deep[q]=deep[p]+1;也就是说q是从p的路径上直接过来的,p和q直接没有其他字符。那么q节点原本不一定可以接收新字符成为后缀,但p可以接收后缀x,如果当前经过p直接来到q,就可以看作是在A的某个后缀后面插入了x(现在q就是那个x),并且在下一次插入的时候,q也可以接收后缀(因为它现在可以被视为x的结点了),所以就把np的fail指向q。

    2、另外一种情况显然就是p和q之间仍然存在其它字符,如果仍然按照情况1来假设,那么就不一定可以保证性质2了。其实也并不难解决,给他一个万能节点nq,nq的作用就是用来替代q的大部分功能,让nq和p连接成为p的儿子,把这种情况模仿成情况1相同的做法(deep[nq]==deep[p]+1)。

看图解吧!以下是完整的构造图解(实边是son边,虚边是fail):

    构造的字符串为ACADD:

    (1)插入A

    

    (2)插入C:节点C的fail只能是根,所以C的fail指向root,deep=2。 注意在寻找fail的过程中,root和A都和C相连。

    (3)插入A:节点A找last,也就是C,然后找到p指针所在的root,判断是情况1还是2,显然是1。直接进行操作。

     

      操作后新插入的new A节点的fail就是root的儿子节点A(第一个),那第一个A就有了两个身份,1是后缀A的末尾字符,2是后缀ACA的末尾字符(代替了new A,保证状态最简,root就不用和new A相连)。

       

      (4)插入D(情况1)

         

      (5)插入D(情况2)

         

      确定了是情况2,那么就要建立nq节点,图中可以很好的体现nq代替q的大部分功能(节点q的指针给nq,nq的fail改为p,q和np的fail改为nq)。

         

      最后,从当前p开始根据fail边往上跳,边把问到的儿子为q的结点的儿子改为nq。(实际上就是保证性质2成立)

         

      BINGO!~终于完成啦~~~

     PS: 感谢大佬functioner%%%

    学会了后缀自动机,开始做题吧:

    这道模板题其实很简单,主要是利用了right集合的性质,很容易就可以想到一个子串right集合的大小其实就是出现的次数,dp求一下就ok啦。


【参考程序】

 #include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#define N 500005
#define ll long long
using namespace std;
char s[N];
int root,cnt,last;
int a[N],Rsort[N],f[N],sa[N],fail[N],deep[N],r[N];
struct SAM
{
int son[];
}ch[N];
void add(int k)
{
int x=a[k];
int p=last,np=++cnt;//上一个节点,新节点
deep[np]=k;
while(p> && ch[p].son[x]==)ch[p].son[x]=np,p=fail[p];
//不停往上跳p,如果我跳到的节点包含当前字符 停止
if(p==)fail[np]=root;//如果我没有另一条返回的路径,那么就直接指向根
else
{
int q=ch[p].son[x];//q记录已选择的返回节点
if(deep[p]+==deep[q])fail[np]=q;//如果q和p正好在一条路上(深度比q小1),直接指向它
else//否则...GG~~ (建一个万能的新节点)
{
int nq=++cnt;//新节点
deep[nq]=deep[p]+;//这个新节点和p节点同一路径,深度+1
ch[nq]=ch[q];fail[nq]=fail[q];//替代q的功能
fail[np]=fail[q]=nq;//让新节点和可选择的节点都指向nq
while(ch[p].son[x]==q)ch[p].son[x]=nq,p=fail[p];//维护 让nq替代q大部分的功能
}
}
last=np;
}
int main()
{
scanf("%s",s+);
last=root=++cnt;
int len=strlen(s+);
for(int i=;i<=len;i++)a[i]=s[i]-'a';//转化为数字,为什么???
//答:“因为要放进数组啊!”
for(int i=;i<=len;i++)add(i);//建立SAM
//将深度基排
for(int i=;i<=cnt;i++)Rsort[deep[i]]++;
for(int i=;i<=len;i++)Rsort[i]+=Rsort[i-];
for(int i=;i<=cnt;i++)sa[Rsort[deep[i]]--]=i;//sa表示深度排名为i的位置 //处理right集合
for(int i=,p=root;i<=len;i++)p=ch[p].son[a[i]],r[p]++;//从小到大按顺序处理子节点
for(int i=cnt;i;i--)r[fail[sa[i]]]+=r[sa[i]];//反过来更新父亲 //dp f[i]就是最终答案
memset(f,,sizeof(f));
for(int i=;i<=cnt;i++)f[deep[i]]=max(f[deep[i]],r[i]);
//从小到大枚举所有的点,每个点的deep值代表一个字串长度,用right集合更新
for(int i=len;i;i--)f[i]=max(f[i+],f[i]);//维护一下正确答案 for(int i=;i<=len;i++)printf("%d\n",f[i]);
return ;
}

SAM学习笔记的更多相关文章

  1. 【文文殿下】后缀自动机(Suffix Automaton,SAM)学习笔记

    前言 后缀自动机是一个强大的数据结构,能够解决很多字符串相关的(String-related)问题. 例如:他可以查询一个字符串在另一个字符串中出现的所有子串,以及查询一个字符串中本质不同的字符串的个 ...

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

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

  3. 后缀自动机(SAM) 学习笔记

    最近学了SAM已经SAM的比较简单的应用,SAM确实不好理解呀,记录一下. 这里提一下后缀自动机比较重要的性质: 1,SAM的点数和边数都是O(n)级别的,但是空间开两倍. 2,SAM每个结点代表一个 ...

  4. SAM学习笔记&AC自动机复习

    形势所迫,一个对字符串深恶痛绝的鸽子又来更新了. SAM 后缀自动机就是一个对于字符串所有后缀所建立起的自动机.一些优良的性质可以使其完成很多字符串的问题. 其核心主要在于每个节点的状态和$endpo ...

  5. <老友记>学习笔记

    这是六个人的故事,从不服输而又有强烈控制欲的monica,未经世事的千金大小姐rachel,正直又专情的ross,幽默风趣的chandle,古怪迷人的phoebe,花心天真的joey——六个好友之间的 ...

  6. Jade学习笔记

    初学nodejs,折腾过用handlebars做模板,后来隔了一段重新学习,用了jade,真心简洁……记录一些学习笔记,以备复习. jade是基于缩进的,所以tab与space不能混用: 属性的设置: ...

  7. 《C++ Primer Plus》学习笔记6

    <C++ Primer Plus>学习笔记6 第11章 使用类 <<<<<<<<<<<<<<<&l ...

  8. Java8学习笔记----Lambda表达式 (转)

    Java8学习笔记----Lambda表达式 天锦 2014-03-24 16:43:30 发表于:ATA之家       本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人 ...

  9. JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue

    前言:俗话说“金三银四铜五”,不知道我要在这段时间找工作会不会很艰难.不管了,工作三年之后就当给自己放个暑假. 面试当中Collection(集合)是基础重点.我在网上看了几篇讲Collection的 ...

随机推荐

  1. win10 + vs2017 + vcpkg —— VC++ 打包工具

    vcpkg 是微软 C++ 团队开发的在 Windows 上运行的 C/C++ 项目包管理工具,可以帮助您在 Windows 平台上获取 C 和 C++ 库. vcpkg 自身也是使用 C++ 开发的 ...

  2. WCF WEB HTTP请求 WCF REST FUL

    首先上点概念WCF 很好的支持了 REST 的开发, 而 RESTful 的服务通常是架构层面上的考虑. 因为它天生就具有很好的跨平台跨语言的集成能力,几乎所有的语言和网络平台都支持 HTTP 请求, ...

  3. 当接口上配了 FeignClient 和 RequestMapping 两个注解,结果错误提示 重复mapping处理方法

    再接手老文档的时候,发现有这么一个问题 错误显示为: 原文档写法: 解决方法: 这是一个编译时写法的问题,将上方的RequestMapping去掉,然后把路径放在下面的PostMapping 便可以正 ...

  4. QT+VTK 对接使用

    由于MFC和pcl的不兼容问题,只能用QT和VTK进行程序开发,确实是一件蛋疼的事! 出自于QT与VTK结合系列:http://blog.csdn.net/tonylk/article/details ...

  5. RabbitMQ学习笔记(6)----RabbitMQ 持久化和非持久化

    持久化:将交换机或队列数据保存到磁盘,服务器宕机或重启之后依然存在. 非持久化:将交换机或队列的数据保存到内存中,服务器宕机或重启之后数据将不存在. 在RabbitMQ中也提供了持久化和非持久化方式. ...

  6. 优动漫PAINT核心功能介绍

    优动漫PAINT是一款功能强大的动漫绘图软件,适用于个人和专业团队创作,分为个人版和EX版.搭载了绘制漫画和插画所需的所有功能——丰富的笔工具.超强的笔压感应和手颤修正功能,可分别满足画师对于插画.漫 ...

  7. 认识优动漫PAINT,优动漫PAINT基本功能有哪些?

    优动漫PAINT是一款搭载了绘制漫画.插画所需所有功能的软件.拥有笔感自然真实.表现形式多样的画笔工具,及高效.完美.便捷的上色工具等. 本文将通过由优动漫PAINT描绘的作品为例,简单介绍该软件的功 ...

  8. Day 08 字符编码

    字符编码 计算机基础 启动应用程序 1.双击QQ 2.操作系统接受指定然后把该操作转化为0和1发送给CPU 3.CPU接受指令然后把指令发给内存 4.内存接受指令把指令发送给硬盘获取数据 5.QQ在内 ...

  9. Python笔记9-----不等长列表转化成DataFrame

    1.不同长度的列表合并成DataFrame. 法1: ntest=['a','b'] ltest=[[1,2],[4,5,6]] 先变成等长的列表:(a:1),(a:2),(b:4),(b:5),(b ...

  10. 数据结构(3) 第三天 栈的应用:就近匹配/中缀表达式转后缀表达式 、树/二叉树的概念、二叉树的递归与非递归遍历(DLR LDR LRD)、递归求叶子节点数目/二叉树高度/二叉树拷贝和释放

    01 上节课回顾 受限的线性表 栈和队列的链式存储其实就是链表 但是不能任意操作 所以叫受限的线性表 02 栈的应用_就近匹配 案例1就近匹配: #include <stdio.h> in ...