前言
原本以为后缀自动机与后缀数组,大致是(AC)自动机与(KMP)的关系。
然而... ...
后缀自动机的板子似乎比后缀数组的板子还短?(一脸懵逼)
好吧,后缀自动机的确比后缀数组简单许多。
简介
后缀自动机是个神奇的数据结构,它存储着一个字符串所有后缀。
它长得有点像一个后缀版的(AC)自动机,但两者还是有些不同的。
例如后缀自动机的一条边上可以有多个字符,而(AC)自动机一条边上只有一个字符。
又如后缀自动机的(fail)指针((Father)数组)是一边插入字符一边更新的,而(AC)自动机则是最后统一建的。
诸如此类的小细节还有很多,这里就不一一举例了。
后缀自动机与(AC)自动机一样有一个基本性质:从根出发,走到每一个终止节点的路径恰好是所有的字符串(后缀自动机的字符串特指后缀),且保证不重不漏。
用后缀自动机可以干许多事,如判断字符串是否出现、不同子串个数等等。
下面,让我们来认识一下后缀自动机吧。
(Father)数组
后缀数组的大致思想就是将字符串的字符一个个插入后缀自动机中,并注意每插入一个字符就要对后缀自动机内的信息进行维护。
对于每个节点,我们用一个变量(len)存储字符串长度,(Father)存储上一个节点的编号,(Son)存储当前节点能到达的节点列表。
而其中最核心的便是每个节点的(Father),它的定义如下:
- 如果存在若干后缀是当前后缀的后缀,则我们选择其中最长的一个后缀作为当前节点的(Father)。
- 如果不存在,则当前节点的(Father)为根节点。
明确了(Father)的概念,我们就可以开始学习后缀自动机的基本思路了。
基本思路
假设我们要插入一个元素(x)。
首先,我们新建一个节点(now),为当前即将插入的新节点。
它的(len)就是上次插入的节点(lst)的(len)加(1)。
接下来,我们用一个变量(p)指向(lst),然后我们就可以把(lst)更新为(now)了。
然后我们判断(p)是否有编号为(x)的儿子,如果没有,就更新(p)编号为(x)的儿子为(now),并更新(p)为其(Father)。
重复此操作直至(p=0)或者(p)有一个编号为(x)的儿子。
判断(p)是否为(0),如果(p=0),则我们令(now)的(Father)为我们事先构造好的根节点(1)号节点,然后退出函数。
不然,我们用(q)记录(p)编号为(x)的儿子,然后判断(q)节点存储的(len)是否恰好是(p)节点的(len)加(1),如果是,则无需构造新节点,可直接令(now)的(Father)为(q),然后退出函数。
否则,我们在(p)与(q)之间新建一个节点(k),作为当前插入节点的(Father)。
初始化(k)的信息,令(k)的(len)为(p)的(len)加(1),且其(Father)与(Son)皆从(q)复制过来。
然后更新(now)和(q)的父亲为(k)。
还有比较重要的一点是,要将(p)及其祖先所有与(q)相连的边全部改成与(k)相连,方便以后的状态查找。
操作流程
以下便是上述内容的具体实现。
首先,我们要定义一个结构体来存储后缀自动机上的每一个节点:
struct Status
{
int len,Father,Son[CHAR_SET_SIZE+5];//len存储字符串长度,Father存储上一个节点,Son[i]存储其能到达的节点列表
}node[(SAM_SIZE<<1)+5];//注意数组要开两倍
然后便是(Insert)函数:
inline void Insert(int x)//增加一个元素
{
register int p=lst,q,k,now=lst=++tot;node[now].len=node[p].len+1,node[now].Size=1;
while(p&&!node[p].Son[x]) node[p].Son[x]=now,p=node[p].Father;//如果p!=0,且没有一个编号为x的儿子,就不断向上跳
if(!p) return (void)(node[now].Father=1);//如果p=0
if(node[p].len+1==node[q=node[p].Son[x]].len) return (void)(node[now].Father=q);//如果q节点存储的len是否恰好是p节点的len加1
node[k=++tot].len=node[p].len+1,node[k].Father=node[q].Father,node[now].Father=node[q].Father=k,memcpy(node[k].Son,node[q].Son,sizeof(node[q].Son));//新增一个节点k作为now的Father,并初始化其信息
while(p&&!(node[p].Son[x]^q)) node[p].Son[x]=k,p=node[p].Father;//将p及其祖先所有与q相连的边全部改成与k相连
}
如何实现询问
以上便是建后缀自动机的全部内容了。
然后我们要考虑一个十分现实的问题:后缀自动机如何实现询问?
注意到后缀自动机节点与其(Father)的连边是一棵树,因此,我们可以将其先按照(len)进行一遍基数排序,然后按(len)从大到小,向根节点传递信息。
最后的答案,就储存于根节点中。
代码(板子题)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000000
#define LL long long
#define Gmax(x,y) (x<(y)&&(x=(y)))
using namespace std;
int n;char s[N+5];
class SuffixAutomation//后缀自动机
{
private:
#define C 26
int tot,lst,v[N<<1],t[N<<1];struct node {int L,Sz,F,S[C+5];}O[N<<1];
I void Rsort()//基数排序
{
RI i;for(i=1;i<=tot;++i) ++t[O[i].L];
for(i=1;i<=n;++i) t[i]+=t[i-1];
for(i=tot;i;--i) v[t[O[i].L]--]=i;
}
public:
I SuffixAutomation() {tot=lst=1;}//初始化
I void Insert(CI x)//插入字符
{
RI p=lst,q,k,now=lst=++tot;O[now].L=O[p].L+1,O[now].Sz=1;
W(p&&!O[p].S[x]) O[p].S[x]=now,p=O[p].F;
if(!p) return (void)(O[now].F=1);
if(O[q=O[p].S[x]].L==O[p].L+1) return (void)(O[now].F=q);
O[k=++tot]=O[q],O[O[q].F=O[now].F=k].L=O[p].L+1,O[k].Sz=0;
W(p&&!(O[p].S[x]^q)) O[p].S[x]=k,p=O[p].F;
}
I LL GetAns()//求答案
{
RI i;Reg LL res=0;for(Rsort(),i=tot;i;--i)
O[v[i]].Sz^1&&Gmax(res,1LL*O[v[i]].L*O[v[i]].Sz),O[O[v[i]].F].Sz+=O[v[i]].Sz;//向上更新信息
return res;
}
}S;
int main()
{
RI i;for(scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;++i) S.Insert(s[i]&31);
return printf("%lld",S.GetAns()),0;
}