初学后缀自动机

前言

原本以为后缀自动机后缀数组,大致是(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;
} 
原文地址:https://www.cnblogs.com/chenxiaoran666/p/SuffixAutomation.html