zoukankan      html  css  js  c++  java
  • 有关后缀自动机

    后缀自动机是一类自动机,基于单个字符串建立,可以在输入该串的任何一个子串后保证不转移至null状态输入该串任何后缀后保证会到达一个is_end状态;亦可作其他用途,在用途上与后缀数组有些类似

    广义后缀自动机,是将多个字符串的后缀自动机建在一起

    一些约定:

    由于自动机存在图论结构,于是本文中常常用点来代指后缀自动机中的状态,用边来代指后缀自动机中的转移,以加强文章的可读性和趣味性

    在本文和其他有关文献中,常用init表示自动机起始状态,常用null表示自动机吃错药后到达的状态,常用is_end表示自动机一个可作为结果的状态,在后缀自动机中则表示从init状态输入一个后缀字符串后转移到的状态;

    本文有些地方在陈述“所有状态时”把null排除在外,这些地方往往容易根据语境分辨。

    然而本文中将一些其他的在其他文献中常见的符号直接用相应的名称代替,一方面是转换输入法十分麻烦,另一方面是见名知意容易理解;

    最后还是希望读者在阅读本文之前,有一定的关于自动机的知识储备。

     

    后缀自动机

     

    性质

    这里指最小后缀自动机,也即在满足后缀自动机的上述基本性质的前提下,使状态和转移数尽可能小的一类后缀自动机

    可以做到状态数和转移数均为线性(就是O(n))

    直接把所有后缀提出来建成trie(或AC自动机)会使状态数达到$n^2$级,十分不好,

    这时后缀自动机是棵树,考虑某种压缩方法;

    先理解后缀自动机的基本要求:

    输入任何子串不转移到null;输入任何后缀会到一个is_end状态

    发现只要满足第一条,就可以保证在输入一个后缀后至少没有转移至null,于是把这个转移到的点标记is_end即可满足第二条,可见第一条是第二条的前提;

    此外,如果我们深究一下,可以看出每个状态都至少可以通过在初始状态输入某一个子串来到达输入两个内容相同的子串应该到达同一个状态

    需要使每一个内容不同的子串转移到不同的状态吗?不需要,因为这样的话,状态数取决于模板串的内容,难以保证状态数线性;

    考虑将初始节点输入某些子串后到达的状态压缩成一个

    能否把某些状态压缩在一起,

    取决于压缩后是否仍与原后缀自动机效果相同,

    即:

    若初始状态init接受子串S到达了点x;

    考虑S在模板串中出现的位置,这里用右端点的位置表示为{r1,r2,r3...rk},

    那么假如这些右端点后一位的字符分别为{c1,c2,c3...ck},

    那么点x后会接代表上述字符的转移;

    同时,从点x开始,输入由{r1+1,r2+1,r3+1,rk+1}为左端点的子串后,转移必须到达非null;输入其他字串后,转移必须到null

    如果S’也想用x表示,那么S’在在模板串中出现的位置也需要满足上述条件;

    那么好像只能保证S‘出现位置与S完全一致才可,

    这样我们考虑将所有出现位置一致(即右端点集合一致)的子串从init转移到的点重在一起

    这样我们便对最小后缀自动机有了一个基本的印象:

    最小后缀自动机:一个后缀自动机,且满足所有右端点集合一致的子串会转移至同一个状态

    这样构成了一个DAG

    为了加深对后缀自动机性质的认识,作如下思考

    在模板串中找到字符c出现的所有位置,把字符c0看做字符串S0,则c0出现的所有位置即构成S0的右端点集合,

    找出c字符出现的所有位置前一位出现的所有字符,若他们是同一个字符c1,则字符串S1=c1+c0的右端点集合与S0一致

    如果他们是不同的字符{c10,c11,c12,c13...c1k},则字符串{c10+c0,c11+c0,c12+c0,...c1k+c0}的右端点集合是S0的一个不重不漏的划分;

    对第二层出现的每一个字符串——不管他们是一个还是多个,重复我们对第一层的字符串(也就是单个字符c0)的操作,找出第三层的字符串,

    不断重复找出第四层、第五层直到无以为继,

    我们考虑建立一个额外的结构,从第i层找到的所有字符串向第i-1层找到的字符串连边,第一层向一个表示空串的节点连边(空串可以看做生成了第一层所有字符串,即单个字符字符串的第零层字符串)

    这些边表示了一个后缀关系,同时表示了一个右端点集合的包含关系,产生了一个树形结构,我们将其命名为朴素的parent树

    如果我们把所有没有分支的树链所成一个点,即如果串S在他出现在模板串中的所有情况中,前一位的字符都是同一个字符c,则把S串与cS和为同一个点,

    或者说如果串A和串B出现情况相同,则把他们并为同一个点;

    这样就有了一个新的、优秀的、每个非叶结点至少两个子节点的parent树

    这样我们构造的这个parent树的点集合与最小后缀自动机的非null状态集合同构

    (每个点与后缀自动机中代表字符串与其一致的状态对应,空串点与初始状态对应)

    这样我们可以证明最小后缀自动机状态数为线性——

    由于最小后缀自动机状态数等于parent树的点数,

    所以只需证明parent树的点数为线性;

    由于parent树的每个叶节点代表的字符串集需包含一个(只出现一次)前缀,于是叶节点个数不大于n个

    而且每个点至少包含两个儿子

    于是parent树的点数不大于2n

    (想想线段树的空间)

    于是与之相等的最小后缀自动机状态数不大于2n

    所以是线性的

    举上述思考过程,一方面可以顺便证明最小后缀自动机状态数线性

    另一方面揭示了下述事实

    在最小后缀自动机中,状态x所代表的子串{S1,S2,S3...Sk}存在一个顺序{a1,a2,a3...ak},使$S_{a_i}$是$S_{a_{i+1}}$的后缀,而且$S_{a_i}$比$S_{s_{i+1}}$前端只少一个字符

    一方面状态x所代表的子串是朴素parent树上的一段树链上所有点一个不少的缩成的,在朴素parent树上子节点与父节点是一个字符之差,

    另一方面从"最小后缀自动机中同一个状态表示的所有右端点集合相同的字符串"的角度反证也可得出这个结论

    这个结论说明在状态x上记录下x所代表的所有字符串中最短的字符串的长度、最长的字符串的长度、右端点有哪些即可保证,即使我们不通过输入字符串从起始状态转移,也可知道这个状态代表哪些字符串,从而把他与其他状态区分开

    而这对最小后缀自动机的建立很有帮助

    事实上x所代表的所有字符串中最短的字符串的长度等于它在parent树上的父节点最长字符串长度+1,于是可以不必记下。

    x右端点有哪些,取决于他在parent树上的对应点所辖子树有哪些叶节点——因为每个叶节点只出现了一次,他们可以对应一个右端点

    从上述事实上可以看出parent树对构造最小后缀自动机十分重要

    (事实上当我们把最小后缀自动机构建完成后,即使将parent树,最长的字符串的长度、右端点有哪些全部抹去,也不影响匹配功能)

    (然而这些东西使最小后缀自动机附加了许多用于匹配之外的功能,所以也十分重要)

    之前我们已经证明了最小后缀自动机的状态数线性,上限不超过2n,

    接下来我们将证明最小后缀自动机的转移数线性

    把最小后缀自动机看做一个DAG,他有且只有起始状态入度为0,考虑在其上割去一些边使这个DAG看起来像一棵树,还得保证从起始状态可以去到任意点(可以认为是以起始状态为根的有根树),

    这样剩下的树边小于2n条,

    考虑割去的非树边的个数,

    这样,我们确定要割哪些边后,按DAG的拓扑序割他们

    当我们割去e1(u1,v1)后,仍然有其他从init起的路径经过v1

    于是,当我们割去同一条init~>is_end路径上两条边时,

    设在这条路径上更靠近init的边为e1(u1,v1),另一个为e2(u2,v2);

    于是先割去e1,这时仍有路径到达v1,进而到达u2,

    于是在割e2前,仍有至少一条条从init到某个is_end的路径经过e2,且他是联通的,

    而割去e2后,这条路径便中断了,

    这意味着

    在按拓扑序割非树边时,任意一条非树边在割掉时,都会额外终结至少一条init到is_end的路径;

    从init到is_end的路径个数等于后缀数,

    所以非树边的个数不超过n

    于是最小后缀自动机的转移数——树边加非树边数——小于3n;

    构建

    下面我们回顾一下后缀自动机有的性质:

    。。。。。。

    好的,回顾完了。

    接下来我们讨论一下一种线性的建后缀自动机的方法;

    与许多自动机的建立方法类似(如回文自动机)

    这种建立方法是考虑在已经建好字符串S后考虑如何建S后连接一个字符c

    考虑Sc比S多了哪些子串,

    发现Sc的所有后缀要么是S中所没有的,要么比从前多出现了一次

    其中最长的后缀,也就是Sc本身,必然是新出现的(长度太长)

    于是新建一个节点new,

    他的转移如何,

    首先他应该由S的最长后缀所代表的点las连一条c的边过来;

    意为他可以代表后缀Sc;

    实际上所有Sc的后缀都是S的后缀(或空串)后连一条c边转移来的;

    (S的后缀可以通过遍历parent树上las到根的树链来得到,这条树链上的每个点都是)

    于是不光是最长的后缀,所有S的后缀中没有c边的都应该连一条c边,

    这些c边都应该连向new,因为他们所代表字符串的右端点集与Sc最长的那个后缀一致,

    于是,new现在意为Sc的所有只在串尾出现过的后缀;

    所有S的后缀中没有c边状态在parent树上是连续的,

    因为当一个点的右端点集一定是其parent的右端点集的子集,所以他能接的边他的parent也都能接;

    于是如果一个点存在c边,他的所有祖先也一定都存在;

    如果S的一个后缀S'所属的状态x本就有一条c边,则意味着后缀S'c并非新出现的后缀,新加入一个c只是使原来就有的子串S‘c的右端点集合多并上一个{|Sc|};

    这样可以直接把new的parent连向x走c边到达的状态xc,是为xc的右端点集合的内容被扩充吗吗?

    未必可以,因为xc状态所代表的子串并非只有S'c,还可能有S’c的后缀以及以S'c为后缀的字符串,诚然S‘c和他的后缀因为多加c而多出现了一次,但是以S‘c为后缀的字符串可能没有

    如果节点xc代表的都是Sc的后缀,这个表现为max(xc)=max(x)+1,那么当然可以直接把new的parent置为xc;

    如果xc代表的字符串只有一部分是Sc的后缀,把xc拆成两个点,其中一个代表的子串的长度较长的区间,应该是非后缀部分(因为若长度为len的串是后缀,长度为len-k的所有子串都是他的后缀,也应该是后缀;若长度为len的串不是后缀,则长度为len+k的字符串以他为后缀,自然也不是后缀)

     

     总结一下后缀自动机的构建;

    当建完S的后缀自动机后,要加入一个c,

    1设最长后缀所属的点为las;

    2新建一个点new;

    3从下向上遍历las和他的祖先,直到有一个点有c边;

    4把所有没有c边的点连一条c边向new;

    5如过las的所有祖先包括init都没有c边,置new的parent为init,结束;

    6如果第一个有c边的点x,走c边到达xc,有max(x)+1=max(xc),置new的parent为xc,结束;

    7否则,把xc拆成max(xc)=原max(xc),max(xc')=max(x)+1,两部分,parent(xc')=parent(xc),parent(xc)=parent(new)=xc';

    这样便完成了。

    例题

    百度搜索:hzwer后缀自动机~~(不要加这个~~)

  • 相关阅读:
    IoC之Ninject
    C#中的扩展方法
    Office 2016 Pro Plus Project 专业版 Visio 专业版 64 位vol版本方便KMS小马oem
    Microsoft Office 2016 简体中文 Vol 版镜像下载
    svn 被锁住 冲突 Can't revert without reverting children
    定时检查服务批处理,发现服务停止立即启动服务
    C++中关于[]静态数组和new分配的动态数组的区别分析
    IP地址与无符号整数值相互转换
    算法:整数与ip地址转换
    将字符串表示的IP地址转变为整形表示
  • 原文地址:https://www.cnblogs.com/nietzsche-oier/p/8331372.html
Copyright © 2011-2022 走看看