前言:
我学习这个东西吧是很懵的,结合了多篇论文和博客才搞懂最基础的构建,需要细细琢磨。
推荐论文:
陈立杰冬令营上的论文,有些小错误,而且我讨厌指针。
张天扬集训队的论文,里面讲了许多应用。
推荐博客:
后缀自动机的定义:
我也没搞清楚。
有限状态自动机的就是能识别字符串。
而后缀自动机就能识别一个字符串的所有后缀,当然同时能识别子串。
后缀自动机的构建:
一个显然的想法是把所有后缀扔到一个叫AC自动机的东西里去。
时间复杂度:…
空间复杂度:…
你懂的~~
我们要构建的自动机自然不是上面这种劣质产品,而是最简状态的后缀自动机。
有多简呢?时空复杂度:O(n)
*接下来的构造讲的非常简略,可以观赏。
son[N][26]表示子节点。
pre[N]表示上一个能接受后缀的点。
step[N]表示从root到这个点的最长距离。
last表示最后一个能接受后缀的点。
假设现在的字符串是T,要在T的后面加一个x.
可以新建一个np代表x。
last沿着pre链条,如果son里没有x所代表的字符,就指向np。
直到点p,p的x所代表字符的子节点已经有了,设这个点为q,此时不能直接指过去,因为会覆盖掉q。
思考p能接受后缀意味着什么:
这表明,从root到p的所有路径都是T的后缀。
1.step[q] = step[p] +1
由于到p的路径都是T的后缀,而step[q] = step[p] +1,保证了要想到q,要么经过p,要么直接从root来。
后缀自动机有一个很重要的性质,就是对于一个点w,能直接转移到它的点一定在一条pre链上。
那么如果有w能够转移到q,w所代表的也是后缀。
因此可以直接将q作为新的可以接受后缀的点。
2.step[q]>step[p]+1
此时如果直接像前面一样,不一定可行,因为可能夹杂其他字符。
怎么办呢?
可以建一个nq到x后面,使step[nq]=step[p]+1,那么就和前面的作用一样了。
即把q的son和pre都copy给nq,np、q的pre只能是nq,
最后把p的pre链上的点的子节点是q的变成nq。
这样p的pre链上的就往nq跑了,其他的就往q跑了。
复杂度分析:
空间复杂度因为每次最多加两个点,显然O(n)。
按pre边建树。
从root出发,沿着树边走到u。
u走一条非树边到v,接着走,一定能走到一个后缀。
可以说成每一条点会对应一条非树边。
所以总边数是O(n)。
注意这里是没有考虑不同字符数的常数的。
Code:
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#define fo(i, x, y) for(int i = x; i <= y; i ++)
#define mem(a) memset(a, 0, sizeof a)
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))
using namespace std;
const int N = 5e5 + 5;
struct suffix_automation {
char s[N];
int son[N][26], pre[N], step[N], last, tot;
void push(int v) {step[++ tot] = v;}
void Extend(int c) {
push(step[last] + 1);
int p = last, np = tot;
for(;p && !son[p][c]; p = pre[p]) son[p][c] = np;
if(!p) pre[np] = 1; else {
int q = son[p][c];
if(step[q] > step[p] + 1) {
push(step[p] + 1);
int nq = tot;
memcpy(son[nq],son[q],sizeof son[q]);
pre[nq] = pre[q]; pre[q] = pre[np] = nq;
for(; son[p][c] == q; p = pre[p]) son[p][c] = nq;
} else pre[np] = q;
}
last = np;
}
void Build() {
scanf("%s", s);
tot = last = 1;
mem(son); mem(pre); mem(step);
for(int i = 0, E = strlen(s); i < E; i ++) Extend(s[i] - 'a');
}
} suf;
int main() {
suf.Build();
}