后缀自动机,是一个能识别一个字符串的所有后缀的自动机。
考虑一种最简单的实现:
将一个字符串的所有后缀插入到一棵trie中。
如图:
可以发现后缀树每个点对应一个后缀集合,每个点对应的后缀集合是这个点子树中所有结束节点所表示的后缀的集合。
把每个点对应的后缀集合称为righ(x)
由于一个字符串的本质不同子串是$O(n^2)$级别的,因此后缀树的节点数是$O(n^2)$级别的。
考虑优化:
首先,给每个结点添加trs指针,表示在一个字符串前加一个字符能转移到的结点。
pre指针表示在前面去掉一个字符转移到的节点。
如图:
结束节点用红色标出,有一个以上儿子的节点用绿色标出,即使根只有一个儿子也将根标记为绿色。
我们把所有白点以及他的指针压缩到下面最近的一个有色点,这样我们就得到了这个串的后缀自动机。
红点的pre一定是红点,因为后缀的前面去掉一个字符后仍然是后缀。
绿点的pre一定是绿点,因为原来有一个分支,去掉后仍然有分支。
所以,白点的trs一定是白点。
可以发现,每个有色点和上面一段白点trs的存在性是相同的。并且这些白点trs到达的结点在压缩之后都相同。
所以,在匹配过程中,这些白点就可以看作相同的,压缩为一个点后不会对结果产生影响。
可以理解为白点仍然存在,只不过为了节省空间,被隐藏了。
可以发现,红点有n个,绿点相当于每次把若干个红点集合合并,最多合并n次就会变成一个集合。
所以压缩之后点数是$O(n)$的。
可以证明,边数也是$O(n)$的。
其实就是暴力后缀trie的每个后缀的虚树。
现在考虑如何构造这个后缀自动机。
对于每个结点,保存它在压缩后的树中的父结点,它对应串的长度 (只保留这一段被压缩的点中,最下面的点(即有色点)的长度),以及它的trs。
下文中的“对应的字符串”均指这一段被压缩的点中,最下面的点对应的字符串。”
使用增量构造法:假设当前已经建出了关于字符串S的后缀自动机,考虑构建关于字符串cS的后缀自动机。
设last为上一次构造的结尾,np为当前构造后的结尾。
分三种情况讨论:
情况1:根不存在为c的儿子。
这种情况,说明若将cS插入后缀树,将变为如下情况:
添加trs指针后:
压缩后:
(这里忽略的点的颜色)
就是说,如果根不存在为c的儿子,即last及他的祖先中没有trs( c )的指针,那么可以直接给根加一个np的儿子。
对于trs指针,只需要把last到根的所有trs( c )赋成np就行了。
情况2:
根存在为c的儿子。
找到last第一个存在trs( c )的祖先p(一定存在这样的祖先,因为根存在trs( c )),然后将从last到p以下的部分的trs( c )赋成np。(原因同情况1,因为这些trs( c )在p下面,np上面,这些点一定是白点,压缩后都会变成np)。
设q=trs(p,c),此时有两种情况:
- $len[q]=len[p]+1$
如图:
说明q是一个有色点,即q对应的字符串是cp。由于p是last的祖先,所以p是S的前缀,所以cp是cS的前缀。所以root到q是np的一个前缀。
因此只需要把np接在q的下面,对于trs指针无需做其他修改。
- $len[q]!=len[p]+1$
这说明,q对应的字符串并不是cp,而cp是一个被压缩到q上的白点。
设这个白点为nq,则q变为nq的儿子。
现在,root到q不是np的一个前缀。
证明:假设root到q是np的一个前缀,那q的pre一定在last到p的路径上,由于last到p以下的点没有trs( c ),所以假设不成立。
所以,root到q不是np的一个前缀。
因此nq是一个分叉点(绿点)。需要把这个点新建出来。
nq的父亲指向q原来的父亲,q和np的父亲指向nq。
此时trs(p,c)应指向nq,并且p祖先的trs( c )如果原来指向q,现在应指向nq。
原因:既然trs(p,c)指向q,所以p祖先的trs( c )指向的应该是q的祖先。
若p祖先的trs( c )原来指向q,说明它实际指向q上面的一个白点(且原来下面最近的一个有色点是q)。
新建nq后,这个白点下面最近的一个有色点就变为了nq,所以现在应指向nq。
并且,trs( c )原来指向q的p的祖先,一定是一段连续的节点。
因为trs( c )!=q,就说明下面又出现了一个新的有色点,而再往祖先走,下面还有这个有色点,所以trs( c )不可能变为q。
新建nq后,这个白点下面最近的一个有色点就变为了nq。
所以这些trs现在应指向nq。
因为nq原来是一个q的白点,因此nq的trs与q相同。只需要将q的trs赋给nq。
如图:
代码:
void insert(int c)
{
int np=++sl,p=la;
len[np]=len[la]+1,la=np;
while(p!=0&&trs[p][c]==0)
{
trs[p][c]=np;
p=fa[p];
}
if(p==0)//情况1
fa[np]=1;
else
{
int q=trs[p][c];
if(len[q]==len[p]+1)//情况2
fa[np]=q;
else//情况3
{
int nq=++sl;
len[nq]=len[p]+1;
fa[nq]=fa[q];
fa[np]=fa[q]=nq;
for(int i=0;i<26;i++)
trs[nq][i]=trs[q][i];
while(p!=0&&trs[p][c]==q)//一定是区间
{
trs[p][c]=nq;
p=fa[p];
}
}
}
red[np]=true;//标记为后缀树上的红点
}
对于这种建树方法,在建树时要从前向后,查询时也从前向后,相当于建一个反串的后缀自动机。
同时注意点数要开到二倍。
例题:【模板】后缀自动机
建出后缀自动机,求出right集合大小,用len*size更新结果即可。
代码:
#include <stdio.h>
#include <stdlib.h>
int fa[2000010],trs[2000010][26],len[2000010],sl=1,la=1;
int fr[2000010],ne[2000010],qz[2000010];
int v[2000010],bs=0;
bool red[2000010];
void addb(int a,int b)
{
v[bs]=b;
ne[bs]=fr[a];
fr[a]=bs;
bs+=1;
}
void insert(int c)
{
int np=++sl,p=la;
len[np]=len[la]+1,la=np;
while(p!=0&&trs[p][c]==0)
{
trs[p][c]=np;
p=fa[p];
}
if(p==0)
fa[np]=1;
else
{
int q=trs[p][c];
if(len[q]==len[p]+1)
fa[np]=q;
else
{
int nq=++sl;
len[nq]=len[p]+1;
fa[nq]=fa[q];
fa[np]=fa[q]=nq;
for(int i=0;i<26;i++)
trs[nq][i]=trs[q][i];
while(p!=0&&trs[p][c]==q)
{
trs[p][c]=nq;
p=fa[p];
}
}
}
red[np]=true;
}
int si[2000010];
int dfs(int u)
{
if(red[u])
si[u]=1;
for(int i=fr[u];i!=-1;i=ne[i])
si[u]+=dfs(v[i]);
return si[u];
}
char ch[1000010];
int main()
{
scanf("%s",ch);
for(int i=0;ch[i]!=0;i++)
insert(ch[i]-'a');
for(int i=1;i<=sl;i++)
fr[i]=-1;
for(int i=1;i<=sl;i++)
addb(fa[i],i);
dfs(1);
long long ma=0;
for(int i=1;i<=sl;i++)
{
if(si[i]>1)
{
long long t=len[i];
t*=si[i];
if(t>ma)
ma=t;
}
}
printf("%lld",ma);
return 0;
}
按照定义,走trs指针相当于在前面添加一个字符,走fa指针相当于在后面去掉一些字符(压缩的原因)。
因为建的是反串的后缀自动机,所以走trs指针相当于在后面添加一个字符,走fa指针相当于在前面去掉一些字符。
所以,子串能够匹配的集合求法:在fa数组构成的树上,进行遍历,right集合就是子树中红点的集合。
因为在fa数组构成的树上向子树中走,就相当于在前面添加字符,而红点其实对应前缀的集合(因为是反串)。
定位子串的快速算法:倍增定位,方法如下:
先找到子串的前缀的位置,再走fa边,就是不停的在前面去掉字符,直到找到子串。
这个过程可以基于长度,树上倍增实现。
例题:[NOI2018]你的名字
先考虑68分的做法:
求在A串中出现,且在B串中没出现的串的数量。
使用容斥,用A的不同子串数减去A,B的不同公共子串数。
先用双指针,求出A的每个位置开始,在B中最多能向后匹配多远。
然后,问题变为,给你一些区间,问它们的子区间中有多少不同的串。
因为每个串,都是原区间$[l,r]$中$[l,i]$的后缀。$(l<=i<=r)$
而后缀就是在前面去掉一些字符,就是不断走fa。
所以定位出这些位置,然后求这些点到根的路径的并。
求树链合并即可,要注意压缩的问题。
100分做法:与68基本相同,就是判断子串$[a,b]$是否出现多了一个限制($l<=a,b<=r$)。
而一个串匹配的位置,就是这个点子树中红点的集合。
红点对应前缀,所以这个限制可以转化为前缀位置的限制。
需要判断一个点的子树中是否有x~y的数。使用DFS序+主席树即可。
代码:
#include <stdio.h>
#include <stdlib.h>
#define ll long long
int fr[2000010],ne[2000010];
int v[2000010],bs=0;
int wl[2000010],wr[2000010],tm=1;
int xl[2000010];
struct SAM
{
int trs[2000010][26],fa[2000010];
int len[2000010],np,sl;
int red[2000010];
SAM()
{
np=sl=1;
}
void clean()
{
for(int i=1;i<=sl;i++)
{
fa[i]=len[i]=0;
red[i]=false;
for(int j=0;j<26;j++)
trs[i][j]=0;
}
np=sl=1;
}
void insert(char c,int wz)
{
c-='a';
int p=np;
np=++sl;
len[np]=len[p]+1;
while(p!=0&&trs[p][c]==0)
{
trs[p][c]=np;
p=fa[p];
}
if(p==0)
fa[np]=1;
else
{
int q=trs[p][c];
if(len[q]==len[p]+1)
fa[np]=q;
else
{
int nq=++sl;
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
len[nq]=len[p]+1;
for(int j=0;j<26;j++)
trs[nq][j]=trs[q][j];
while(p!=0&&trs[p][c]==q)
{
trs[p][c]=nq;
p=fa[p];
}
}
}
red[np]=wz;
}
};
void addb(int a,int b)
{
v[bs]=b;
ne[bs]=fr[a];
fr[a]=bs;
bs+=1;
}
SAM S,T;
void dfs1(int u)
{
xl[tm]=u;
wl[u]=tm++;
for(int i=fr[u];i!=-1;i=ne[i])
dfs1(v[i]);
wr[u]=tm;
}
int up[1000010];
char zf[1000010];
struct SPx
{
int u,cd;
SPx(){}
SPx(int U,int Cd)
{
u=U;
cd=Cd;
}
};
SPx px[1000010];
ll baoli(int s)
{
for(int i=1;i<=T.sl;i++)
up[i]=0;
for(int i=0;i<s;i++)
{
int cd=px[i].cd,u=px[i].u;
while(u!=1)
{
if(up[u]==T.len[u]-T.len[T.fa[u]])
break;
if(cd-T.len[T.fa[u]]>up[u])
up[u]=cd-T.len[T.fa[u]];
u=T.fa[u];
cd=T.len[u];
}
}
ll rtn=0;
for(int i=2;i<=T.sl;i++)
rtn+=up[i];
return rtn;
}
int he[24000010],cl[24000010],cr[24000010],sl=0;
int jianshu(int l,int r)
{
int rt=sl++;
he[rt]=0;
if(l+1==r)
return rt;
int m=(l+r)>>1;
cl[rt]=jianshu(l,m);
cr[rt]=jianshu(m,r);
return rt;
}
int xiugai(int i,int l,int r,int j,int x)
{
int rt=sl++;
if(l+1==r)
{
he[rt]=he[i]+x;
return rt;
}
cl[rt]=cl[i];
cr[rt]=cr[i];
int m=(l+r)>>1;
if(j<m)
cl[rt]=xiugai(cl[rt],l,m,j,x);
else
cr[rt]=xiugai(cr[rt],m,r,j,x);
he[rt]=he[cl[rt]]+he[cr[rt]];
return rt;
}
int chaxun(int i,int l,int r,int L,int R)
{
if(R<=l||r<=L)
return 0;
if(L<=l&&r<=R)
return he[i];
int m=(l+r)>>1;
return chaxun(cl[i],l,m,L,R)+chaxun(cr[i],m,r,L,R);
}
int gen[1000010],n;
bool zichuan(int i,int l,int r,int cd)
{
l=l+cd-1;
if(l>r)
return false;
int z=chaxun(gen[wr[i]-1],1,n+1,l,r+1)-chaxun(gen[wl[i]-1],1,n+1,l,r+1);
return z>0;
}
int main()
{
scanf("%s",zf);
for(n=0;zf[n]!=0;n++)
S.insert(zf[n],n+1);
for(int i=1;i<=S.sl;i++)
fr[i]=-1;
for(int i=1;i<=S.sl;i++)
addb(S.fa[i],i);
dfs1(1);
gen[0]=jianshu(1,n+1);
for(int i=1;i<tm;i++)
{
if(S.red[xl[i]]!=0)
gen[i]=xiugai(gen[i-1],1,n+1,S.red[xl[i]],1);
else
gen[i]=gen[i-1];
}
int m;
scanf("%d",&m);
for(int i=0;i<m;i++)
{
int l,r,x=1,y=1,s=0;
scanf("%s%d%d",zf,&l,&r);
T.clean();
for(int j=0;zf[j]!=0;j++)
T.insert(zf[j],0);
for(int j=0,k=-1,c=0;zf[j]!=0;j++)
{
c-=1;
if(x!=1&&k-j==S.len[S.fa[x]])
x=S.fa[x];
if(y!=1&&k-j==T.len[T.fa[y]])
y=T.fa[y];
if(k<j)
k=j,c=0;
while(zf[k]!=0&&S.trs[x][zf[k]-'a']!=0&&zichuan(S.trs[x][zf[k]-'a'],l,r,c+1))
{
c+=1;
x=S.trs[x][zf[k]-'a'];
y=T.trs[y][zf[k]-'a'];
px[s++]=SPx(y,c);
k+=1;
}
}
ll zo=0;
for(int j=1;j<=T.sl;j++)
zo=zo+T.len[j]-T.len[T.fa[j]];
printf("%lld\n",zo-baoli(s));
}
return 0;
}
后缀自动机图例:
ddbabbaa: