zoukankan      html  css  js  c++  java
  • 初学后缀自动机

    前言

    原本以为后缀自动机后缀数组,大致是(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;
    } 
    
  • 相关阅读:
    [C++] WinAES问题
    MDCC 2014移动开发者大会 小礼品展商活动
    JavaScript模式读书笔记 文章3章 文字和构造
    《深入了解Android:Wi-Fi、NFC和GPS音量》勘误表
    PHP, Python, Node.js 哪个比较适合写爬虫?
    java模拟浏览器包selenium整合了htmlunit,火狐浏览器,IE浏览器,opare浏览器驱
    论文密级
    HttpAsyncClient 做并发长连接的一个实例
    Java利用httpasyncclient进行异步HTTP请求
    使用异步HTTP提升客户端性能(HttpAsyncClient)
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/SuffixAutomation.html
Copyright © 2011-2022 走看看