后缀自动机基础应用

本博客讲解后缀自动机基础应用,而不说明定义、构建等内容。
构建代码如下:(跑得飞慢)

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节点是前缀节点,对应。其他*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;
}

∣ R i g h t ∣ |Right| Right的意义是这个状态中的串出现了多少次。
若要统计本质不同不重叠重复子串个数,可以记录的最左位置和最右位置进行判断。

统计子串

在后缀自动机DAG上从root到一个点的一段路径是一个子串。
在后缀自动机DAG上,一个点能到达的点的数量代表以这个状态为前缀的不同子串个数。一个点能达到的点的 ∣ R i g h t ∣ |Right| Right之和代表以这个状态为前缀的子串个数
以下为求可到达子串的代码:(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节点的状态。

求T的所有前缀的最长的在S中出现的后缀

对S建后缀自动机,T在自动机DAG上匹配。记录cans表示当前T的前缀匹配的最长后缀的长度。每次匹配T上的一个字符时若ch[o][c]存在则cans增加1,否则跳parent树上的父亲并判断ch[o][c]是否存在,若到根节点都不存在则cans=0,否则cans=len[o]+1。因为parent树上的祖先一定是当前匹配段o的后缀,所以直接取其最大长度。

广义后缀自动机

将多个串建在一个后缀自动机上。
有部分特性。(本月)

广义后缀自动机有两种写法:Trie树上后缀自动机(离线)和Trie上后缀自动机(在线)。

Trie树上后缀自动机

重写于2018.10.8:
广义后缀自动机是对Trie树建的后缀自动机。对于多个主串,我们先建出Trie树,然后bfs Trie树,记录ch[i]表示i号结点到其父亲的边的字符,df[i]表示i号结点,triefa[i]表示i号结点树上父亲。最后用nod[i]表示Trie树结点i在广义后缀自动机上的节点编号。按bfs序(拓扑序)将Trie树上的结点加入后缀自动机,对于第i个节点传3个参数,o=df[i],last=nod[df[triefa[i]],ch=ch[i]。
这种写法的复杂度是理论下界。

Trie树后缀自动机 upd on 2018.12.25

建议只学习这一种
这种写法复杂度为 O ( n n ) O(n\sqrt n) O(nn ),但目前没有有效的方法将其复杂度卡满。
每次插入串后,将lst结点重新赋为root。对extend()函数做若干修改,传两个参表示lst和新的字符c,每次插完字符返回新的lst。若插入时没有c转移边则与一般的后缀自动机一样,否则若len[ch[lst][c]]=len[lst]+1则返回ch[lst][c],否则建立克隆节点并返回。

建立广义后缀自动机时没有统计信息。统计信息一般有以下方法:

统计子串

为每个状态维护cnt和lsts两个值,表示出现次数和上次更新该状态的字符串。
枚举每个串的每个字符对应的广义后缀自动机结点,从下往上更新该节点到根的Parent树上的路径,如果lsts=当前字符串就break,否则cnt++并更新lsts。

动态维护Parent树信息

如果只用维护Parent树当然不用LCT。但我们要维护每个点的 ∣ R i g h t ∣ |Right| Right。前文的做法是在建完后缀自动机后再线性统计的,不能支持修改操作。
用LCT维护Parent树,可以支持在线在主串末尾添加字符串的操作并维护每个节点的 ∣ R i g h t ∣ |Right| Right。如果有其他信息也可以维护。

维护所有状态的Right集合

在Parent树上,一个节点的Right集合是它所有儿子节点的Right集合的并。
启发式合并set线段树合并或主席树做。主席树做法是按dfs序记录每个叶节点的right,查询是dfs序上的一段区间。

如果只用输出Right集合的所有元素,则遍历这个节点在parent树上的子树统计前缀节点个数即可。因为子树大小和答案规模是同阶的。

连接子串的问题

可用f[c]表示若干子串并的最后一个字符是c的答案。

子串对应的结点

右端点在后缀自动机上跑,左端点在parent树上倍增。


mip xtc caff soi ab gos sor HAHAHA!

2018.9.19注:写博客结果把这篇覆盖了,在百度快照上找回来,结果格式都没了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值