后缀自动机是一个很复杂的算法,下面讲一讲建图和应用。
建图
首先要明确后缀自动机上点和含义:一个点代表的是一类有相同后缀的字符串,而每一个点的fail一定是它本身的后缀。
那么我们定义如下结构体:
struct point
{
int len;
int fa;
int ch[30];
};
point a[MAXN];
其中fa就是fail,ch表示点的出边(a~z),len就表示这个点能表示的最大后缀长度。
然后对于一个字符串,我们每次加入一个点。
设当前点为np,之前的主干上的最后一个点为p,当前加入的字符为c。
显然,a[np].len=a[p].len+1(因为从a[p].len出来加上一个c字符就到了np)。
接着我们就要求fail,这一个要分类讨论。
我们从np开始跳fail链,要保证跳到的点的没有c出边,对于这些点,我们把它的c出边设为np。
然后如果直接跳到了1都还没有发现一个有c出边的点,那么就把a[np].fail设为1(这个是case1)。
如果找到了一个有c出边的点,设它的出边连向q,那么就判断一下a[q].len==a[p].len+1(注意此时p是那个有c出边的点)。
如果相等,那么a[np].fa=q,这一个是没问题的了(case2)。
否则我们还要新建一个nq,使a[nq].len=a[p].len+1,然后把q的出边信息全部赋给nq。接下来,a[nq].fa=p,a[q].fa=nq。最后把p的fail链上的c出边连向q的点的c出边连向nq。
这么做是因为q的后缀未必就是np的后缀(因为a[q].len!=a[p].len+1,即可能加上很多个字符),所以我们不能直接把a[np].fail赋为q,我们需要新建一个nq节点。
以上就是建图了,时间复杂度是O(n)级别的。
贴一下代码:
int insert(int c)
{
int i,j,p,np,q,nq;
p=last;
ne++;np=ne;
a[np].len=a[p].len+1;
for(;p>=0&&a[p].ch[c]==0;p=a[p].fa)a[p].ch[c]=np;
if(p==0)a[np].fa=1;
else
{
q=a[p].ch[c];
if(a[q].len==a[p].len+1)a[np].fa=q;
else
{
ne++;nq=ne;
a[nq]=a[q];
a[nq].len=a[p].len+1;
a[q].fa=nq;a[np].fa=nq;
for(;a[p].ch[c]==q;p=a[p].fa)a[p].ch[c]=nq;
}
}
last=np;
}
应用
首先我们要知道一个性质:从1点走出的任意一条路径都是原串的子串。
然后有一下几个常见的问题:
- 判断一个串是否为原串的子串
直接从1开始走即可
- 求一共有多少个子串
这是一个范围很广的问题,它要分相同的子串是否算同一个。
首先我们设f[i]表示i点代表的字符串有几个。若相同的子串算同一个,那么f[i]=1显然。否则我们就要花点时间求一求f[i]了。
求f[i]时,我们发现对于每一个包含前缀的点(即每一次的np),它的fail链(即它的所有后缀)上的字符串的出现次数都要加1.
这样就可以了。具体操作就是f[np]=1,然后用差分的方法把f都加上(即f[a[x].fail]+=f[x])。
至于为什么这样做是对的,我们可以想一下,每一个np就是一个前缀,而它的fail链就是前缀的后缀,所以这样就可以覆盖所有的子串了。
首先我们设size[i]表示从i点开始往下走可以走出多少条路径。那么size[x]=sum(size[y])+f[x](y为x可到达的点)。这个方程的含义就是x可以走向任何一个y可以走到的字符串,并且不走的话它本身还有f[x]个字符串。
求出了size之后,很多问题(例如查排名、求个数)都可以解决了。
注意:
- 在查排名的时候每到一个z,k就要减去f[z]。因为就算我接下来不往下走,我也会得到f[z]的字符串。
- 求f是根据fail来dfs,求size是根据出边来dfs。
- 后缀自动机的空间一般开2n。
来源:https://blog.csdn.net/chiyankuan/article/details/98934432