后缀自动机的构造算法采用的是逐个在字符串后面添加字符的在线做法
令
p
为当前字符串最长的那个后缀(就是当前整个字符串)所代表的节点,现在我们要添加一个字符
- 加入
c
之后的整个串必然构成一个新节点,因为他右端点集合只有
curlen 一个,而 curlen 在之前的节点的右端点集合中必然没有出现过。不妨记为 np 。考虑所有 p 的祖先,也就是加入c 之前所有当前后缀,他们现在后面多了一个字符 c ,我们的目标就是要让这些新产生的子串仍然能被后缀自动机识别。记任意一个p 在par树上的祖先为 p0 ,分两种情况讨论:
-
ch[p0][c]==null
,那么对于任意一个属于
p0
的串
s
,s+c必然属于
np ,这种情况只需要设置 ch[p0][c]=np 即可. - ch[p0][c]!=null 这表示之前就有一个子串是当前串的后缀,我们不需要改变什么就已经能识别 s+c 。
-
ch[p0][c]==null
,那么对于任意一个属于
p0
的串
s
,s+c必然属于
- 接着,我们需要找到这个新节点在
par
树上的父亲。也就是我们需要找到一个最长的后缀,他在当前串中至少出现两次。由于当前的后缀必然是上一次的后缀之后添加一个字符
c
得到,因此,我们可以遍历
p 的祖先,找到第一个具有 c 边的节点,记这个节点为p′ ,令 ch[p′][c]=q 这里又有两种情况.
-
q
这个节点所代表的所有子串都可以由
p′ 代表的某个子串 +c 得到,即 ml[q]==ml[p]+1 那么这个 q 就是我们要求的,只需设置f[np]=q 即可. -
ml[q]>ml[p]+1
,这表示节点
q
代表的某些子串并不是当前的后缀,解决方法也很简单,只需要单独的把那些是当前后缀的从
q 中分裂出来,记分裂出来的节点为 nq ,那么需要改变:
- 将
nq
设置为
q
的
par树中的父结点 - 将
q
的所有出边都复制一份给
nq - 将所有指向
q
的边都改成指向
nq ,由于所有指向 q 的边必然来源与p的par树上一段连续的区间,在上面修改即可 - f[np]=nq
- 将
nq
设置为
q
的
-
q
这个节点所代表的所有子串都可以由
- 从构造算法中可以看出,每添加一个字符,最多新增两个节点,因此节点总数不会多于 n+n
#include<bits/stdc++.h>
using namespace std;
const int Maxn=123456;
struct suffix_automaton
{
int ml[Maxn],f[Maxn],ch[Maxn][26];
int sz,last;
void init()
{
memset(ch[0],0,sizeof(ch[0]));
last=sz=1;
}
int getidx(char c){return c-'a';}
void add(char x)
{
int c=getidx(x);
int p=last,np=sz++;ml[np]=ml[p]+1;last=np;
memset(ch[np],0,sizeof(ch[np]));
while(p&&!ch[p][c])ch[p][c]=np,p=f[p];
if(!p){f[np]=0;return;}
int q=ch[p][c];
if(ml[q]==ml[p]+1){f[np]=q;return;}
int nq=sz++;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
ml[nq]=ml[p]+1;
f[nq]=f[q];
f[q]=nq;
while(p&&ch[p][c]==q)ch[p][c]=nq,p=f[p];
f[np]=nq;
}
}solver;
int main()
{
solver.init();
}