根到每个节点的每一条路径对应每一种子串,无一重复,无一遗漏。
关于自动机中的一个节点的大小,就是其所能表示的最长子串的长度。
一个节点表示的子串都是后缀关系,即其中一个是另一个的后缀。他们的长度连续,并可以由一个右端点联系起来。
当节点a表示的最长子串是节点b表示的最长子串的后缀,节点a可以由节点b跳到,可以看做是对一种蕴含关系的遍历。另外,b可以把自己的右端点分享给a。
当一个节点表示的一个子串是别的点表示的子串的后缀,而该节点表示的其他子串可能不是,就得把这个节点拆开来,需要哪个就把哪个当儿子。
附一份优雅的代码
struct suffix_automaton
{
string s;
int son[maxn][26],pre[maxn],step[maxn],last,total;
inline void push_back(int v)
{
step[++total]=v;
}
void Extend(char ch)
{
push_back(step[last]+1);
int p=last,np=total;
for (; !son[p][ch]; p=pre[p]) son[p][ch]=np;
if (!p) pre[np]=1;
else
{
int q=son[p][ch];
if (step[q]!=step[p]+1)
{
push_back(step[p]+1);
int nq=total;
memcpy(son[nq],son[q],sizeof(son[q]));
pre[nq]=pre[q];
pre[q]=pre[np]=nq;
for (; son[p][ch]==q; p=pre[p]) son[p][ch]=nq;
} else pre[np]=q;
}
last=np;
}
void Build()
{
cin>>s;
total=last=1;
memset(son,0,sizeof(son));
memset(pre,0,sizeof(pre));
memset(step,0,sizeof(step));
for (int i=0,End=s.size(); i!=End; i++) Extend(s[i]-'A');
visit(1,0);
}
} suf;
我想具体的Kyle会讲的==。
这就是后缀自动机。
参考文献( 有图哦(:з」∠) ):
后缀自动机学习总结-functioner
后缀自动机讲解-Oyking
还有后缀自动机的性质:
后缀自动机具有两大性质,考虑转移边为自动机,考虑父亲边为逆序后缀树(后缀数组为后缀树的dfs序),如果忽略字符集大小的常数,构造复杂度为o(n)
很好很强大,更深的性质呢?
对于一个节点i,根s
1、整个自动机可以接受字符串的所有子串,且自动机为一个有向拓扑图
2、一条到i的路径唯一对应一个子串
3、所有i可以接受的状态互相成后缀关系,且长度连续
4、对于i的父亲rt[i],rt[i]所能接受的所有子串都是i所能接受的所有子串的后缀
构造后缀自动机的时候有一步是判断l[a]+1==l[b],为的是防止多余状态被自动机接受,如果l[a]+1==l[b],那么b所能接受的最长子串是合法的,根据3,4可知所有其他状态也都是合法的,否则l[a]+1!=l[b]可能会出现不合法状态,那么新建节点使l[c]=l[a]+1就可以筛出合法状态,而rt指针的修改可以看作是逆序后缀树的压缩边被解压缩出一个节点并添加一个新的叶子节点(即新逆序后缀)
5、统计方式的理解。从后续题目来看有两种统计方法,一是根据转移边dp,二是根据父亲边dp,其实是统计两个不同东西,转移边dp可以统计出有多少子串以此节点接受的状态为前缀,根据父亲边dp可以统计出以该节点接受的最长子串为后缀的子串有多少(逆序后缀树i所代表的是i所接受最长子串)
6、其实字典树也可以建立后缀自动机,考虑我们对于串建的时候,记last是为了找出最后一个的最长子串,而在字典树上就是字典树的父亲,所以按dfs序及父亲就可以建出字典树的后缀自动机
7、(逆序后缀树i所代表的是i所接受最长子串)这句话的意思是,将该串所有的逆序后缀建立一棵后缀树,而自动机中的每个节点i,它在后缀树中一直沿父亲边走向根\所代表的逆序后缀==节点i在自动机中接受到的最长子串
总而言之,后缀自动机兼有两种特性,如果想知道后缀的公共前缀之类,或子串重复次数等等与后缀联系的上的,可以用后缀树的性质,如果与所有子串相关,自动机就比较好用