【自动机】

   有限状态自动机的功能是识别字符串,自动机A能识别字符串S,就记为$A(S)$=true,否则$A(S)$=false。

   自动机由$alpha$(字符集),$state$(状态集合),$init$(初始状态),$end$(结束状态集合),$trans$(状态转移函数)组成。

   令$trans(s,str)$表示当前状态是$s$,读入字符串或字符$str$后达到的状态。

   从状态$s$开始能识别的字符串$x$满足$trans(s,x)\subset end$。

【后缀自动机(SAM)】

   SAM(suffix automaton)是一个能识别字符串$S$的所有后缀的自动机。

   即$SAM(x)=true$,当且仅当$x$是$S$的后缀。

【有限状态后缀自动机】

   有限状态后缀自动机是状态数最少的后缀自动机,大小为$O(n)$。

   令$ST(a)=trans(init, a)$

   如果字符串$a$在$S$中的$[l,r)$中出现,那么它就能识别$S$从$r$开始的后缀。

   如果$a$在$S$中出现的位置为$\{[l_1, r_1),[l_2, r_2), ..., [l_n, r_n)\}$,那么$a$能识别的字符串为$\{suffix(r_1), suffix(r_2), ..., suffix(r_n)\}$。

   令$right(a)={r_1, r_2, ..., r_n}$,那么$a$能识别的字符串完全由$right(a)$决定,也就是说如果$right(a)=right(b)$,则有$ST(a)=ST(b)$。

   所以一个状态$s$,由所有right集合为$right(s)$的字符串组成。

   只要给定一个$Right$集合,再给定一个长度,就确定了一个子串。

   一个$Right$集合所对应的子串长度是一个区间,换句话说,如果对于一个$Right$集合,若长度$l$和$r$合适,则对于满足$l\leq x \leq r$的长度$x$也合适。因为如果$x$确定的子串$a_x$的某个出现位置不在$Right$集合中,那么$r$确定的子串$a_r$作为$a_x$的后缀,肯定也有某个位置不在$Right$中。如果$Righr$中的某个位置无法确定$a_x$,那么$a_x$作为$a_l$的后缀,也会有某个位置无法确定$a_l$。

   于是令$[min(s), max(s)]$表示状态s的长度区间。

【状态数的线性证明】

   假设两个状态$a, b$,假设$right(a)$和$right(b)$有交集。

   因为$a,b$不同,所以这两个状态表示的子串无交集,$[min(a), max(a)]$和$[min(b), max(b)]$也无交集。因为如果有交集,那么他们的$Right$集合必定相等,就变成同一个状态了。

   因为$right(a), right(b)$有交集,设$min(a)>max(b)$,那么$b$表示的所有子串长度都比$a$的小,而右端点相同,也就是说$b$中所有子串都是$a$中子串的后缀。也就是说$a$所出现的所有位置,$b$都出现了,所以$r_a\subset $r_b$,也就是$r_a$是$r_b$的真子集。

   也就是说要么两个状态的$Right$集合不相交,要么其中一个状态的$Right$集合是另一个的真子集。

   上方是所有状态的$Right$集合的示意图,我们称之为$Parent$树。

   在这个树里,每个节点至少有两个儿子,所以节点的个数是$O(n)$的。

   证明了节点数是$O(n)$的之后,我们还需要证明边数是$O(n)$的。

   考虑一颗$SAM$的生成树(跟$Parent$树)无关。

   令状态数为$M$,一共$M-1$条边,并且一个后缀对应自己遇到的第一条非树边(一条边可能对应多个后缀),刚好能够对应,而后缀个数为$O(n)$的,于是边数也是$O(n)$的。

   我们不可能保存每一个状态的$Right$集合,但是一个状态的$Right$集合能由它子树中的叶子$Right$集合的并集。

   对于一个状态$s$,设$r_i\in right(s)$,$right(trans(s,x))=\{r_i+1|S[r_i]==x\}$。

【线性构造SAM分析】

   设当前字符串为$T$,$T$的长度为$L$,将新加入一个字符$x$。

   设所有表示$T$的后缀(也就是$Right$中包含L)的状态$v_1, v_2, v_3, ...$

   必然存在一个状态$p=ST(T)$满足$right(p)=\{L\}$,因为$v_1, v_2, v_3, ...$都包含$L$,所以他们在$Parent$树上全是$p$的祖先。

   假设我们添加一个字符$x$后,用$np$表示$ST(Tx)$,那么$right(np)={L+1}$。

   设$v1=p, v_2, .., v_k=root$,即按深度递减排序,所以$v_1, v_2, v_3, ...$的$Right$大小递增,且如果$v_i$的某位置出发有$x$的边,那么$v_{i+1}$也有。如果$v_j$出发没有$x$的边,那么直接可以把它向$np$连一条$x$的边,因为它的$Right$集合中有$L$。

   设$v_p$是$v_1, v_2, v_3, ...$中第一个某个位置出发有$x$边的状态,令$trans(v_p, x)=q$,那么$right(q)=\{r_i+1|s[r_i]=x\}$,注意此时$x$还没有加进字符串里。

   难点来了,$x$加进字符串里之后,我们不能直接在$right(q)$中加入$L+1$。

   举例子:

   $T=aaabaaaabaa$, $x=b$ 也就是$Tx=aaabaaaabaab$。

   将$T$中$v_p$代表的某个字符串用括号标记:$a(aa)baa(aa)b(aa)$

   将$T$中$q$代表的某个字符串用括号标记:$(aaab)a(aaab)aa$

   此时加入$b$,会发现$L+1$加入后可以代表$aab$,但是不能代表$aaab$,所以并不能直接在$right(q)$中加入$L+1$。

   当然如果$len(v_p)+1==len(q)$的话也是可以直接加入$L+1$的。

   解决上面问题的方法就是新建一个节点$nq$,显然$right(nq)=right(q)\cup (right(np)=\{L+1\})$,就能够解决这个问题了。

   于是$trans(v_p~v_k, x)=nq,trans(v_1~v_{p-1}, x)=q$,然后再连接一下$Parent$树就完成构造过程了。

【线性构造SAM步骤】

   ①新建节点$np$代表$ST(Tx)$。

   ②自$Parent$树的叶子节点$L$向上找到第一个有出边$x$的$Right$集合包含L的状态$v_p$,途中没有出边$x$的节点都向$np$连边,即$trans(v_1~v_{p-1}, x)=np$。

   ③若没有$v_p$,则$Parent$树上$np$连向$root$

   ④新建节点$nq$,复制一次$q$,并进行以下更新

   $fa(nq)=fa(q)$//此时的$q$是加入$x$以前的$q$

   $fa(q)=fa(np)=nq$

   ⑤$v_p~v_k$向$nq$连边,即$trans(v_p~v_k)=nq$。

   然后就没有了。

   注意点数是$2n$的!

代码如下:

#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn=, inf=1e9;
struct sam{int len, fa, trans[];}st[maxn];
int n, root, tott, now;
char s[maxn];
inline void read(int &k)
{
int f=; k=; char c=getchar();
while(c<'' || c>'') c=='-'&&(f=-), c=getchar();
while(c<='' && c>='') k=k*+c-'', c=getchar();
k*=f;
}
inline void extend(int ch)
{
int np=++tott, p=now;
st[np].len=st[p].len+; now=np;
while(p && !st[p].trans[ch]) st[p].trans[ch]=np, p=st[p].fa;
if(!p) st[np].fa=root;
else
{
int q=st[p].trans[ch];
if(st[p].len+==st[q].len) st[np].fa=q;
else
{
int nq=++tott;
st[nq]=st[q];
st[nq].len=st[p].len+;
st[np].fa=st[q].fa=nq;
while(p && st[p].trans[ch]==q) st[p].trans[ch]=nq, p=st[p].fa;
}
}
}
int main()
{
scanf("%s", s+);
n=strlen(s+); now=tott=root=;
for(int i=;i<=n;i++) extend(s[i]-'a');
}

拓展:http://blog.csdn.net/doyouseeman/article/details/52245413

例题:http://www.cnblogs.com/Sakits/p/8251363.html

【算法】后缀自动机(SAM) 初探的更多相关文章

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

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

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

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

  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) 例题

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

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

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

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

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

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

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

随机推荐

  1. 【刷题】BZOJ 3495 PA2010 Riddle

    Description 有n个城镇被分成了k个郡,有m条连接城镇的无向边. 要求给每个郡选择一个城镇作为首都,满足每条边至少有一个端点是首都. Input 第一行有三个整数,城镇数n(1<=n& ...

  2. Maven添加第三方库及部署配置

    配置其实很简单,还是修改~/.m2/settings.xml文件,具体用文件说话,其他不解释. <?xml version="1.0" encoding="UTF- ...

  3. bzoj4152 The Captain (dijkstra)

    做dijkstra,但只需要贪心地把每个点连到它左边.右边.上边.下面的第一个点就可以了 #include<bits/stdc++.h> #define pa pair<int,in ...

  4. CF1096D Easy Problem(DP)

    貌似最近刷了好多的CF题…… 题目链接:CF原网 洛谷 题目大意:有一个长度为 $n$ 的字符串 $s$,删除第 $i$ 个字符需要代价 $a_i$.问使得 $s$ 不含有子序列(不是子串)" ...

  5. coin

    Decsription 数据范围:\(n<=3000,m<=300\),保证\(\forall i,\sum\limits_{j}p_{ij}=1000\) Solution 日常期望算不 ...

  6. 构造代码块----java基础总结

    前言:之前一直不知道构造代码块的意思是什么,只是知道他的具体的表现形式,因为经常在面试题中看到,所以准备好好写写. 作用: 给对象进行初始化,对象一建立就运行,而且优于构造方法运行. 和构造方法的区别 ...

  7. Hadoop基础-MapReduce的Combiner用法案例

    Hadoop基础-MapReduce的Combiner用法案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.编写年度最高气温统计 如上图说所示:有一个temp的文件,里面存放 ...

  8. Redis记录-Redis高级应用

    Redis数据库可以使用安全的方案,使得进行连接的任何客户端在执行命令之前都需要进行身份验证.要保护Redis安全,需要在配置文件中设置密码. 示例 下面的示例显示了保护Redis实例的步骤. 127 ...

  9. MySql与对应的Java的时间类型

    MySql的时间类型有          Java中与之对应的时间类型date                                           java.sql.Date Date ...

  10. bzoj千题计划187:bzoj1770: [Usaco2009 Nov]lights 燈 (高斯消元解异或方程组+枚举自由元)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1770 a[i][j] 表示i对j有影响 高斯消元解异或方程组 然后dfs枚举自由元确定最优解 #in ...