后缀自动机学习总结

本文详细介绍了后缀自动机的构建过程,包括其数据结构、核心算法及时间空间复杂度分析。通过实例演示了如何逐步构建后缀自动机。

LINK: http://blog.sina.com.cn/s/blog_70811e1a01014dkz.html


星期五的时候,在网上看别人的总结看了很久也没有什么收获,不过星期六看了clj原汁原味的论文,终于明白了(网上的总结只写了大致做法,比较难理解).

后缀自动机实质上是字母树,记录的字符串是某个字符串s的所有后缀.这里以字符串ACADD为例:

后缀自动机学习总结

这样很浪费空间和时间(实际上都是O(n^2)).但是,注意:这棵字母树的结点虽然多,但大部分结点都只有一个儿子,而且有很多段是一样的.那么,利用公共部分,就可以对空间进行压缩,具体地说,就是把自己连到儿子的边删掉(并把该儿子及其后代删掉),再把这条边连到别的子树,这样就能充分利用公共部分,节省空间.但是,如何保证这样做和原来的笨做法是等价的,又如何把时间复杂度和空间复杂度降到O(n)?这是个问题.幸运的是,后缀自动机出现了.

后缀自动机是这样的:在后缀自动机中,为了节省空间,某个点有可能成为多个结点的儿子,可以保证在后缀自动机中遍历出来的所有字符串不会重复,而且刚好是原串s的所有子串.

先讲讲后缀自动机的大致做法:假设当前已经建好了s的某个前缀的后缀自动机t,那么就要通过某种算法,添加一个字符x,得到s另一前缀tx的后缀自动机,这样每次插入一个字符,最后把s的所有字符按顺序插入完毕就得到了s的后缀自动机.

这样的话,建造后缀自动机的过程是在线的,就是说,可以任意时刻询问s的某些信息,也可以任意时刻在s的结尾插入一些字符,变成新的字符串.不过,删除是不支持的.

在后缀自动机中,每个结点储存的信息有:

son[26]:返回该结点对应的子串加上某个字符后生成的合法子串在后缀自动机中所对应的位置(其实就和字母树一样),如果该指针不存在,就说明这样的子串是不存在的(即不是s的子串)

pre:注意这不是返回它的父结点(因为某个点有可能成为多个结点的儿子),而是返回上一个可以接收后缀的结点(如果当前结点可以接收新的后缀,那么pre指向的结点也一定可以接收后缀).

step:返回的是从根结点走到该结点,最多需要多少步.


为了方便下面的叙述,这里先提出三个后缀自动机的性质:

①从root到任意结点p的每条路径上的字符组成的字符串,都是当前串t的子串.

②因为满足性质一,所以如果当前结点p是可以接收新后缀的结点,那么从root到任意结点p的每条路径上的字符组成的字符串,都是必定是当前串t的后缀.

③如果结点p可以接收新的后缀,那么ppre指向的结点也可以接收后缀,反过来就不行.

下面的叙述中,将直接应用这两个性质.


当前建立的后缀自动机是对应字符串t,现在要插入字符x,t的后缀自动机变成tx的后缀自动机.

首先建立储存当前字符x的结点np,找到之前最后一个建立的结点(因为它一定满足性质②),然后就不断按pre指针跳(直到跳到有x儿子的结点为止).

假设当前跳到p结点,如果p没有x儿子,那么它一定可以接收新来的字符,然后就把px儿子赋值为np(这时,p接收了后缀字符x,目前已经不可以接收新的后缀字符了).然后,就要处理有x儿子的结点了,假设px儿子是q.只有2种情况:

step[q]=step[p]+1.

因为我们要后缀自动机的结点尽量少,所以要尽量共用一些信息.这是对应的图:

后缀自动机学习总结

这时,p点是满足性质②的.这时,如果可以把np直接接到p后面,就可以省下很多空间了,但是因为q点的存在, np不能直接接到p后面,否则p-q的信息就丢失了.那么能不能把q当成np?就是说q可不可以像np那样,作为t最后一个字符”,来接收新的后缀呢?答案是肯定的.p可以接收新的后缀,q就不一定能接收新的后缀,这样做会不会有问题?

本来,这样的做法是不行的(这是情况②要解决的问题),step[q]=step[p]+1,保证了:q原本是从p的路径上来的,而且pq之间不会夹杂其它字符.虽然q本来不一定可以接收新的后缀,p可以接收后缀x,如果当前经过p来到q,就可以视为是在t的某个后缀后面插入了x(现在q就是那个x),并且在下一次插入的时候,q也可以接收后缀(因为它现在可以被视为x的结点了),所以就把nppre指向q.

在这里,我有个原来不懂的地方(后来明白了):因为q当前不一定是可以接收后缀的点,现在把它当成了代表x的结点并已经将它变成可以接收新后缀的状态.这对于来到p结点后再走q结点的路径必然是对的(因为来到p结点,就相当于找到了t的一个后缀,现在又找到q结点,就相当于找到一个tx的后缀了),但是如果遍历的时候不经过p就直接到了q,好像就不能保证所在路径对应的字符串是tx的后缀了,这时它还能接收新的后缀吗?

其实, step[q]=step[p]+1就保证了经过q,就一定会经过p;而如果不经过p,就只能从root直接来了.也就是说,保证了到达p的都是后缀.为什么?可以用反证法(我想了1个多钟啊):

假设原命题不成立,那么就有两种可能:

.当前的x字符,之前没有出现过.这样的话,x字符的子串必然是后缀,与假设矛盾.

.当前的x字符,之前已经出现过.这样的话,x字符而不是后缀的子串必然与之前的某个代表字符x的结点连接,而不是与当前的q点连接,否则后缀自动机的性质早就被破坏了,故也与假设矛盾.

综上, step[q]=step[p]+1,保证了到达p的都是后缀.同时这也解释了为什么要找最靠后的一个有x儿子的结点了.

于是,我们就把代表t的后缀自动机改进为代表tx的后缀自动机了.如图(实边是son指针,虚边是pre指针):

后缀自动机学习总结

step[q]>step[p]+1

这和上一种情况一样,也面临着q点是否可以当成x结点的问题.在上一种情况的描述中,我们可以知道, step[q]=step[p]+1可以保证q原本是从p的路径上来的,而且pq之间不会夹杂其它字符,所以可以直接把q结点当成x结点.那么反过来, step[q]>step[p]+1,就说明pq之间有可能会夹杂其它字符,这就不能保证把q当成x结点以后,q的路径都是tx的后缀了,于是我们不能采取和前一种情况一样的做法.但是,我们可以模仿前一种情况的做法.

上面的做法合法,是因为step[q]=step[p]+1,那么如果新建一个结点nq来代替q,同时保证step[nq]=step[p]+1就相当于第一种情况了,这时,只要把qson边和pre边都copynq上即可.但是别忘了把nqpre改为p,再把nqnppre都改为nq.

因为现在nq代替了q,所以npprenq.由性质③可知nqpre只能是p.同样的,qnq也满足性质③,所以qpre只能是nq.

最后,还要再按ppre指针往上跳,son[x]=qp结点改为son[x]=nq(因为nq代替了q).

先贴个程序:

struct suffix_automaton

{

       string s;

       int son[maxn][26],pre[maxn],step[maxn],last,total;

       inline void push_back(int v)

       {

              step[++total]=v;

        }

       void Extend(char ch)

       {

            push_back(step[last]+1);

            int p=last,np=total;

            for (; !son[p][ch]; p=pre[p]) son[p][ch]=np;

            if (!p) pre[np]=0;

             else

             {

                 int q=son[p][ch];

                 if (step[q]!=step[p]+1)

                 {

                    push_back(step[p]+1);

                    int nq=total;

                    memcpy(son[nq],son[q],sizeof(son[q]));

                    pre[nq]=pre[q];

                    pre[q]=pre[np]=nq;

                    for (; son[p][ch]==q; p=pre[p]) son[p][ch]=nq;

                 }  else pre[np]=q;

             }

            last=np;

        }

       void Build()

       {

            fin>>s;

            total=last=0;

            memset(son,0,sizeof(son));

            memset(pre,0,sizeof(pre));

            memset(step,0,sizeof(step));

            for (int i=0,End=s.size(); i!=End; i++) Extend(s[i]-'A');

            visit(0,0);

        }

}suf;

在外部调用suf.Build()即可.

空间复杂度:很明显,每次插入最多增加2个结点,所以是O(n).

时间复杂度:暂时还没算好,但应该是O(n).


下面是ACADD的构造过程(实边是son指针,虚边是pre指针):

①插入A:

后缀自动机学习总结

A的上一个可以接收后缀的点只能是根结点,所以Apre指向root,step=1.

②插入C:

后缀自动机学习总结

C的上一个可以接收后缀的点只能是根结点,所以Cpre指向root,step=2.

pre指针跳跃的过程中,Aroot都连了C.

③插入A:

后缀自动机学习总结

p指针先指向C结点,然后再跳到root,现在rootA儿子,所以检查rootstep值是否等于rootA儿子的step+1.现在判断成功,所以rootA儿子现在有双重身份(后缀“A”的最后一个字符,和后缀”ACA”的最后一个字符),现在是情况①,所以新建立的点的pre连到它即可.而且因为第一个A对于root来说,代替了第二个A,所以root不用往第二个A结点连边.

现在的后缀自动机变成了这样:

后缀自动机学习总结

④插入D:

后缀自动机学习总结

也是往上跳就行了,跳完两个A结点就直接跳到根,都是情况①.完成后就多了3条实边和一条虚边.

⑤插入D:

后缀自动机学习总结

首先确定pq指针,step[q]<step[p]+1随之确定这是第二种情况.

然后建立新结点nq,q的指针copynq,nqpre改为p,qnppre改为nq.

后缀自动机学习总结

最后,p指针一边往上跳,一边把son[x]=qp结点改为son[x]=nq.

后缀自动机学习总结

最后,后缀自动机就诞生了:

后缀自动机学习总结

按字典序遍历一遍 :

A

AC

ACA

ACAD

ACADD

AD

ADD

C

CA

CAD

CADD

D

DD

遍历的结果是:所有子串都按字典序打印出来了,无一重复,也无一遗漏.

如果是不断询问第k小的子串呢,需要在后缀自动机里走k次吗?那太慢了.注意到后缀自动机中,虽然一个结点可能被当成多个结点的儿子,但这些连边都是满足拓扑序的,就是说,可以预处理出到达某个结点时,往下走可以得到多少个字符串,这样的预处理用拓扑排序+递推即可.这样的话,询问就是O(n)的复杂度.不过预处理好像只能是离线,要是在线的话,每次插入了一个字符,又要重新预处理一遍了.


<think>好的,我现在要仔细想想用户的问题:“回文自动机是什么”。首先,我需要确认用户的基础。可能用户已经了解过自动机理论,比如有限自动机(FA)、后缀自动机(SAM)等,但回文自动机可能是一个相对陌生的概念。用户可能是在学习字符串处理相关算法,或者在解决回文问题时遇到了这个数据结构,所以需要了解其基本原理和应用。 首先,我需要回忆回文自动机的定义。回文自动机,也叫作回文树(Palindromic Tree)或Eertree,是由Mikhail Rubinchik和Maxim信息量大的论文提出的,用于高效处理字符串中的所有回文子串的结构。它能够在线性时间内构建,并存储所有的唯一回文子串,同时支持一些查询操作,比如统计不同回文的数量、最长回文等。 接下来,我需要确定回文自动机的结构。回文自动机由节点组成,每个节点代表一个唯一的回文子串。每个节点包含以下信息:回文的长度、后缀链接(指向当前回文的最长真后缀回文)、以及转移边(用于在添加新字符时扩展新的回文)。根节点有两个,分别对应长度为-1和0的虚根,这是为了处理奇偶长度的回文。 然后,构建过程是怎样的呢?比如,假设字符串是“abba”,如何一步步构建自动机。初始时,创建两个根节点。然后逐个字符处理,维护当前最长回文后缀的位置。当添加新字符时,检查是否可以通过当前节点的转移边扩展,如果不行,则创建新节点,并更新后缀链接。这个过程类似于后缀自动机的构建,但专门针对回文结构。 还需要考虑时间复杂度和空间复杂度。根据文献,回文自动机的构建时间复杂度是O(n),其中n是字符串长度。空间复杂度是O(n),因为每个节点代表唯一的回文子串,而一个长度为n的字符串最多有n个不同的回文子串(比如全相同字符的情况,如“aaaaa”有n个回文子串)。 应用方面,回文自动机可以用于统计所有回文子串的数量、找出最长回文子串、计算每个回文出现的次数等。例如,在解决LeetCode上的回文相关问题时,回文自动机可能提供高效的解决方案。 但用户可能需要更具体的例子来理解。比如,构建过程中的步骤,或者如何通过后缀链接快速跳转。可能还需要比较回文自动机与其他方法(如Manacher算法)的异同。Manacher算法用于寻找最长回文子串,而回文自动机则更全面,能够处理所有回文子串的信息。 此外,用户可能想知道如何实现回文自动机。代码结构大致包括节点结构体的定义(包含长度、后缀链接、转移数组),初始化两个根节点,然后逐个字符处理,维护当前节点,并在必要时创建新节点,同时更新后缀链接。 需要注意的细节包括如何处理奇偶长度的回文。两个根节点中,长度为-1的节点用于处理奇长度回文,比如当添加字符时,从该节点出发可以形成长度为1的回文。而长度为0的节点处理偶长度回文。例如,当添加一个字符,当前节点无法扩展时,通过后缀链接回溯,直到找到可以扩展的位置或者到达根节点。 可能遇到的疑问点包括后缀链接的作用。后缀链接指向当前回文的最长真后缀回文,这在构建过程中帮助快速跳转,避免重复检查。例如,当添加新字符后,如果当前节点无法扩展,通过后缀链接找到可能的位置,减少时间复杂度。 最后,总结回文自动机的优势和适用场景。它适合需要处理多个回文子串的问题,尤其是在需要统计或频繁查询的情况下,而不仅仅是寻找最长回文。与动态规划相比,回文自动机在时间和空间上更高效,尤其是当字符串较长时。</think>回文自动机(Palindromic Automaton),又称回文树(Eertree),是一种专门用于高效处理字符串中所有回文子串的数据结构。它能在线性时间内构建,并支持统计唯一回文数量、查询最长回文等功能。 --- ### **核心原理** 1. **节点定义** 每个节点代表一个唯一的回文子串,包含以下信息: - `len`:回文长度 - `fail`:后缀链接(指向当前回文的最长真后缀回文) - `trans[]`:转移边(记录添加字符后形成的新回文) 2. **虚根设计** 有两个特殊根节点: - 虚根1(奇根):长度为 `-1`,用于处理奇数长度回文(如 `"a"`)。 - 虚根2(偶根):长度为 `0`,用于处理偶数长度回文(如 `"aa"`)。 3. **构建过程** 逐个字符处理字符串,维护当前最长回文后缀节点。每添加字符 `c` 时: - 从当前节点出发,沿后缀链接回溯,直到找到可扩展的节点。 - 若扩展成功,新建节点并更新后缀链接。 --- ### **时间复杂度** - **构建复杂度**:$O(n)$,其中 $n$ 为字符串长度。 - **空间复杂度**:$O(n)$,每个节点对应唯一回文子串。 --- ### **应用场景** 1. 统计所有不同回文子串的数量。 2. 查找最长回文子串。 3. 计算每个回文的出现次数。 4. 结合其他算法解决复杂回文问题(如回文分割、动态回文维护)。 --- ### **示例分析** 以字符串 `"abba"` 为例: 1. 初始状态:仅有虚根奇根(`len=-1`)和偶根(`len=0`)。 2. 添加字符 `a`: - 从偶根出发,形成长度为1的奇回文 `"a"`。 3. 添加字符 `b`: - 从 `"a"` 出发,无法扩展,回溯到奇根后新建节点 `"b"`。 4. 添加字符 `b`: - 从当前节点 `"b"` 扩展,形成 `"bb"`(偶回文)。 5. 添加字符 `a`: - 从 `"bb"` 扩展,形成 `"abba"`(偶回文)。 最终,回文自动机包含所有回文子串:`"a"`, `"b"`, `"bb"`, `"abba"`, `"b"`(末尾单独 `b`)。 --- ### **对比其他方法** - **Manacher算法**:仅求最长回文,时间复杂度 $O(n)$,但无法统计所有回文。 - **动态规划**:需 $O(n^2)$ 时间预处理,空间开销大。 - **回文自动机**:综合高效,适合处理多回文查询问题。 --- ### **代码实现要点** ```python class Node: def __init__(self): self.len = 0 # 回文长度 self.fail = None # 后缀链接 self.trans = {} # 转移边(字符→节点) # 初始化虚根 root_odd = Node() root_odd.len = -1 root_even = Node() root_even.len = 0 root_odd.fail = root_even.fail = root_odd # 初始指向自身 ``` --- 回文自动机通过巧妙的后缀链接和转移边设计,将回文处理效率提升到线性级别,是处理复杂回文问题的利器。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值