把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

初学后缀自动机

前言

原本以为后缀自动机后缀数组,大致是\(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;
} 
posted @ 2018-12-20 20:32  TheLostWeak  阅读(272)  评论(0编辑  收藏  举报