简介
后缀——此数据结构维护一个字符串的所有后缀
自动机——一个有向无环图,可以运行,找出所有子串。
所以它是一个能处理与子串有关的强大数据结构。
如果我们用后缀建立一个Trie树,就会是这样↓(串:acadd),空间
这种太浪费空间了,而后缀自动机,就可以重复利用节点,把空间自动机降为
像这样↓
构造过程
一个字符一个字符往里面添加
每次添加一个字符,会导致原串多个后缀增加,为了找到这些后缀,我们给每个节点多定义一个pre,每个节点有以下信息
- son[26]:类似Trie,指向下一个字符节点
- step:根节点到该节点最长路径长度,后面用于区分情况
- pre:指向上一个要接受后一个字符的节点(每次增加字符要把pre的节点表示的后缀都要增加)
全局信息:
- root:根
- last:最后添加的节点
初始一个节点root
添加a
root->son[a]=1
1->pre=root;
添加c
从last一直沿着pre往上跳,如果该节点son[c]是空的,那么就连上新的节点2
1->son[c]=2;
root->son[c]=2;
2->pre=root;
添加a
从last一直往上跳,如果该节点p的son[c]有值,判断son[c]的step值是否为p的step+1,如果是,说明该节点代表的所有子串中,包含p->son[c]是最长的,son[c]代表的其它子串都是最长串的后缀,那么son[c]就可以当做最后一个节点,在以后接受新的节点在后面,那么3->pre=p->son[c];
2->son[c]=3
添加d
添加d
按照pre往上跳,如果发现p的son[c]的step不等于p的step+1,那么就必须新建节点了
新建nq(=6)复制p->son[c]的所有信息,然后使p及p的所有pre连向p->son[c]的边全部连向nq(root->son[c]=nq),nq就代替了p->son[c],而且可以接收新的字符,所以p->son[c]->pre=nq;
5->pre=nq
一些有用的性质
定义一些东西
一个子串的结束位置(endpos)为它在原串所有出现的结束位置集合,如acadd
a的结束位置为{1,3},ad的结束位置为{4}
所有在后缀自动机上走出来的路径,都是原串的子串
从根节点到某个节点的所有路径的子串,endpos集合一样,这些子串的长度为连续的一段自然数,短的为长的串的后缀:
如上图的4节点,有以下子串{“acad”,”cad”,”ad”},长度为2~4,endpos为{4};
这样,每个节点都可以表示一个endpos集合,代表的子串最长长度为step值,最短长度为pre的step+1(此性质可用于求出不同子串数量)
最开始我们用pre是为了方便我们构建后缀自动机,它表示上一个能接收新字符的节点,所以pre表示的子串一定为当前节点的子串的后缀:
如4节点的pre为6,6有子串{d},是{“acad”,”cad”,”ad”}的后缀,长度为1,endpos为{4,5};每
沿着pre走一步,endpos大小就增加1,所以一个节点endpos的大小为有多少个节点pre连向它,可以利用拓扑排序求出每个节点的endpos大小(此性质可用于求出子串出现次数)
(留坑:可能还有很多有用的东西)……
代码
struct Node
{
int mx;
Node *son[30],*pre;
}nodes[MAXL*2],*ncur=nodes+1,*root=nodes,*last=root;
void Insert(int c)
{
Node *p=last,*np=ncur++;
np->mx=last->mx+1;
for(;p&&p->son[c]==NULL;p=p->pre)
p->son[c]=np;
if(p==NULL)
np->pre=root;
else
{
Node *q=p->son[c];
if(q->mx==p->mx+1)
np->pre=q;
else
{
Node *nq=ncur++;
memcpy(nq,q,sizeof(Node));
nq->mx=p->mx+1;
q->pre=np->pre=nq;
for(;p&&p->son[c]==q;p=p->pre)
p->son[c]=nq;
}
}
last=np;
}