【知识点】后缀自动机
后缀自动机:
简介:
通过巧妙的设计使得我们能用一个DAG和树的复合结构来在线性复杂度内存储一个串的$n^2$个子串的信息。
定义:
1.后缀自动机的结构类似于AC自动机,每个点表示一个endpos等价类(子串结束位置的集合,以下简称为状态),边同AC自动机中的边。即后缀自动机上从根到一个点有若干条路径,这些路径构成的子串在原串中的endpos集合都相同。
2.parent树是一棵建立在后缀自动机节点上的树,建立的方法是类似于线段树的分割(见下文)。它满足任意点u代表的任意一个字符串是u子树中任意一个字符串的后缀。
3.一些需要维护的信息:
struct node{ int ch[30]; //边(到某个点可能有很多条路径,它们同属于这个点的状态) int dis; //到达该点的所有路径中最长的那一条的长度(该状态最长的串长) int fa; //父亲(串长严格小于儿子;严格是儿子的后缀,但父子之间不一定没有边) };
性质:
在提到性质之前我们先考虑一下这个endpos等价类的含义。
以$aababa$这个串为例,它有6个可能的endpos,我们现在按长度依次考虑以i为endpos的每个子串$(s[1,i],s[2,i],\cdots ,s[i,i])$所属的endpos等价类。
初始都是空串,那么$endpos(空串)=\{1,2,3,4,5,6\}$。
然后考虑长度为1的子串,位置1,2,4,6是'a',位置3,5是'b'。那么$endpos('a')=\{1,2,4,6\}$,$endpos('b')=\{3,5\}$。
然后考虑长度为2的子串,位置2是'aa',位置3,5是'ab',位置4,6是'ba'。那么$endpos('aa')=\{2\}$,$endpos('ab')=\{3,5\}$,$endpos('ba')=\{4,6\}$。
……
自己模拟一下这个过程,不难发现三点性质:
1.随着长度的增加,endpos等价类从集合$\{1,2,\cdots,n\}$逐渐划分成若干个小集合。但不是完全划分,中间会因为某个前缀的长度不够而损失掉这个前缀。
2.对于任意两个子串,它们的endpos等价类要么互为子集,要么不相交。显然前者是互为后缀的情况,后者是不互为后缀的情况。
3.同一个endpos等价类包含的子串实际上是某个极长子串的所有后缀。因为我们是在逐渐往每个endpos前面塞字符,那么某个等价类里的所有endpos前面塞完一个字符后,它们代表的字符串可能从相同变得不同。此时该等价类被划分,它的极长子串就是塞之前的子串。
构造:
使用增量构造法,就是假设前n-1个字符已经构造好了,考虑插入第n个字符c会怎么样,也就是如何处理插入后新产生的子串(新串的n个后缀)。
先建一个代表$\{n\}$等价类的点np,然后从上一次插入的点last(即代表$\{n-1\}$等价类,$s[1,n-1]$的点)开始跳fa,每跳到一个点p就把它往np连一条字符边c。直到跳到0(不是根)或者当前点p已经有一条字符c出边。
如果跳到0了说明原串里根本就没出现c这个字符,于是直接记$x[np].fa=rt$即可。
否则,设$q=x[p].to[c]$,分q是或不是新串的后缀两种情况讨论。
当且仅当$x[p].dis+1=x[q].dis$时,q是新串的后缀(q里的最长串就是p里的最长串+c)。此时新串长度$>x[p].dis+1$的后缀都连过边c了,而长度$\leq x[p].dis+1$的后缀已经在原串出现过了(就是q代表的所有子串),所以整个系统合法,只需要令$x[np].fa=q$。
否则,q不是新串的后缀(q里的最长串的后缀是p里的最长串+c)。此时我们把q拆成两个点nq和q,前者是新串的后缀(即长度$\leq x[p].dis+1$的部分),后者不是。拆完之后把p所有的祖先原来连到q的c边都改到nq(因为p的祖先+c是新串的后缀),然后记$x[nq].fa=x[q].fa,x[q].fa=x[np].fa=nq$即可。此时整个系统合法。
关于为什么$x[p].dis+1\neq x[q].dis$时q不是新串的后缀:因为如果是,那么q中最长串去掉c后必然是一个长度$>x[p].dis$的原串后缀,且它代表的节点有c出边。那么这个节点应该先于p被遍历到并且停下,而这并没有发生,所以q一定不是新串的后缀。
代码:
#include<bits/stdc++.h> #define maxn 1000005 #define maxm 500005 #define inf 0x7fffffff #define ll long long #define debug(x) cerr<<#x<<": "<<x<<endl #define fgx cerr<<"--------------"<<endl #define dgx cerr<<"=============="<<endl using namespace std; int hd[maxn<<1],nxt[maxn<<1],to[maxn<<1],ans,cnt; char str[maxn<<1]; inline int read(){ int x=0,f=1; char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') f=-1; for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f; } inline void addedge(int u,int v){to[++cnt]=v,nxt[cnt]=hd[u],hd[u]=cnt;} struct SAM{ struct node{int to[30],fa,siz,dis;}x[maxn<<1]; int tot=1,lst=1,rt=1; inline void extend(int pos){ int val=str[pos]-'a',np=++tot,p=lst; lst=np,x[np].siz=1,x[np].dis=pos; for(;p&&!x[p].to[val];p=x[p].fa) x[p].to[val]=np; if(!p) x[np].fa=rt; else{ int q=x[p].to[val]; if(x[q].dis==x[p].dis+1) x[np].fa=q; else{ int nq=++tot; x[nq].dis=x[p].dis+1; memcpy(x[nq].to,x[q].to,sizeof(x[q].to)); x[nq].fa=x[q].fa,x[np].fa=x[q].fa=nq; for(;x[p].to[val]==q;p=x[p].fa) x[p].to[val]=nq; } } } inline void dfs(int u){ for(int i=hd[u];i;i=nxt[i]) dfs(to[i]),x[u].siz+=x[to[i]].siz; if(x[u].siz!=1) ans=max(ans,x[u].siz*x[u].dis); } }at; int main(){ scanf("%s",str+1); int n=strlen(str+1); for(int i=1;i<=n;i++) at.extend(i); for(int i=2;i<=at.tot;i++) addedge(at.x[i].fa,i); at.dfs(1),printf("%d\n",ans); return 0; }
广义后缀自动机:
没啥区别,每次插入一个串的时候直接把last设为1就行了,相当于从头开始插入。
#include<bits/stdc++.h> #define maxn 1000005 #define maxm 500005 #define inf 0x7fffffff #define ll long long #define rint register int #define debug(x) cerr<<#x<<": "<<x<<endl #define fgx cerr<<"--------------"<<endl #define dgx cerr<<"=============="<<endl using namespace std; char str[maxn]; inline int read(){ int x=0,f=1; char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') f=-1; for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f; } struct RAMauto{ int to[maxn<<1][30],dis[maxn<<1],fa[maxn<<1],vis[maxn<<1],tot; inline void insert(int n){ int las=1; for(int i=1;i<=n;i++){ int ch=str[i]-'a',p=las,now=++tot; dis[now]=i,las=now; while(p && !to[p][ch]) to[p][ch]=now,p=fa[p]; if(!p) fa[now]=1; else{ int q=to[p][ch]; if(dis[q]==dis[p]+1) fa[now]=q; else{ int nq=++tot; dis[nq]=dis[p]+1; memcpy(to[nq],to[q],sizeof(to[q])); fa[nq]=fa[q],fa[q]=fa[now]=nq; while(p && to[p][ch]==q) to[p][ch]=nq,p=fa[p]; } } } } inline ll dfs(int u){ if(vis[u]) return 0; vis[u]=1; ll res=(ll)dis[u]-(ll)dis[fa[u]]; for(int i=0;i<26;i++) if(to[u][i]) res+=dfs(to[u][i]); return res; } }RAM; int main(){ int T=read(); RAM.tot=1; while(T--) scanf("%s",str+1),RAM.insert(strlen(str+1)); printf("%lld\n",RAM.dfs(1)); return 0; }