本博客讲解后缀自动机基础应用,而不说明定义、构建等内容。
构建代码如下:(跑得飞慢)
struct node {
node* ch[26], *f;
int len, siz; // siz即Right集合大小
};
node* _nd = (node*)malloc(SIZE_OF_SAM); int noden;
node* root, *last;
inline node* newnode(node* fa) {
node* ret = _nd+(noden++);
if(fa) ret->len = fa->len + 1; else ret->len = 0; ret->siz = 0;
return ret;
}
inline void extend(int x) {
node* o = newnode(last), *p = last; o->siz = 1;
last = o;
for(; p && !p->ch[x]; p = p->f) p->ch[x] = o;
if(!p) {o->f = root; return;}
node* q = p->ch[x];
if(q->len == p->len + 1) o->f = q;
else{
node* nq = newnode(p);
memcpy(nq->ch, q->ch, sizeof(q->ch));
nq->f = q->f; q->f = o->f = nq;
for(; p && p->ch[x] == q; p = p->f) p->ch[x] = nq;
}
}
其中,每次新建的*o节点是前缀节点,对应 |Right|=1 | R i g h t | = 1 。其他*nq节点是非前缀节点。
如不加特别说明,以下fa的意义是parent树上的父亲,len的意义是这个状态对应的最长字符串的长度。由后缀自动机性质,其最短长度为len[fa]+1。后缀自动机DAG的意义是后缀自动机由字符转移边构成的DAG。
拓扑序
根据len的性质,可用以下代码求出后缀自动机DAG的拓扑序:
int topo_b[maxn], topo[maxn];
inline void get_topo() {
for(int i = 0; i < noden; i++) topo_b[(_nd+i)->len]++;
for(int i = 1; i < noden; i++) topo_b[i] += topo_b[i-1];
for(int i = 0; i < noden; i++) topo[--topo_b[(_nd+i)->len]] = i;
}
用拓扑序可以替代用dfs遍历后缀自动机来统计某些信息。
求所有状态Right集合大小
在一开始,只有前缀节点siz=1。
从儿子到父亲统计即可。即对每个状态u,siz[fa]+=siz[u]。
for(int i = noden-1; i >= 0; i--) {
node* o = (_nd+topo[i]);
if(o->f) o->f->siz += o->siz;
}
|Right| | R i g h t | 的意义是这个状态中的串出现了多少次。
若要统计本质不同不重叠重复子串个数,可以记录 Right R i g h t 的最左位置和最右位置进行判断。
统计子串
在后缀自动机DAG上从root到一个点的一段路径是一个子串。
在后缀自动机DAG上,一个点能到达的点的数量代表以这个状态为前缀的不同子串个数。一个点能达到的点的 |Right| | R i g h t | 之和代表以这个状态为前缀的子串个数。
以下为求可到达子串的代码:(siz的定义分两种)注意root的答案只能从儿子合并。
for(int i = noden-1; i >= 0; i--) {
node* o = (_nd+topo[i]);
if(o != root) o->sum = o->siz;
for(int j = 0; j < 26; j++) if(o->ch[j]) o->sum += o->ch[j]->sum;
}
由此我们看到,parent树和后缀自动机DAG各有所用。
[bzoj3998] 求第k大的子串和本质不同子串。
分别对应上述两种情况。查询时类似XX树上二分跳即可。
前缀的最长公共后缀
答案=len[lca(x, y)]。lca指在parent树上的lca。容易理解。
[bzoj3238] 求两两后缀的最长公共前缀。
反建后缀自动机即可。
一建一跑
本质是后缀自动机上的DP。与其他自动机上DP类似,DP状态中记录到自动机中的哪个节点即可。
常用状态:f[i][j][…]表示当前枚举到字符串i位,在自动机上j节点的状态。
广义后缀自动机
将多个串建在一个后缀自动机上。
考虑将多个串建在一起的情况,可以发现一些特性。
同于将多个串连起来建自动机的做法,较于复杂。
参见练习题。
动态维护Parent树信息
如果只用维护Parent树当然不用LCT。但我们要维护每个点的 |Right| | R i g h t | 。前文的做法是在建完后缀自动机后再线性统计的,不能支持修改操作。
用LCT维护Parent树,可以支持在线在主串末尾添加字符串的操作并维护每个节点的 |Right| | R i g h t | 。如果有其他信息也可以维护。
维护所有状态的Right集合
在Parent树上,一个节点的Right集合是它所有儿子节点的Right集合的并。
用动态开点树、平衡树、set等都可以做。
mip xtc caff soi ab gos sor HAHAHA!