广义后缀自动机-GSAM

学习 广义SAM 之前,请保证对 字典树、SAM 有一定了解


基础知识

伪广义后缀自动机

之所以是“伪”,因为它的时间复杂度并无法保证(虽然大部分情况下依旧是线性的,也够用),而且部分信息可能无法维护

常见的有以下两种:

    1. 将多个字符串直接连接,相邻两个字符串用特殊符号分隔
    1. 每将一个字符串全部加入到 SAM 后,将 last 指针清零

(标准)广义后缀自动机

我们都知道,字典树可以维护多个字符串的信息

如果我们直接在字典树上魔改,让它成为一个后缀自动机,它有什么优秀性质?

    1. 对于一个结点 \(u\)\(len_u\) 等于 \(u\) 在字典树上的深度
    1. 将字典树进行拓扑排序(BFS),得到的就是一个 \(len\) 单调不递减的序列

再回想建立 SAM 的过程,我们插入的结点的 \(len\) 单调递增的,且前后差值为 \(1\)last 指针时上一个加入的状态

而广义 SAM 中,当我们加入一个结点时,last 指针就是它在字典树上的父亲

因此我们只需要在原字典树进行修改(如维护后缀链接、最长长度等)


过程

    1. 建立一棵字典树
    1. 对字典树进行 BFS,并记录每个结点的父亲
    1. BFS 的同时,在原字典树构建 SAM

BFS 部分:

#define pii std::pair<int, int>
std::queue<pii> q;
FOR(i, 0, 25) if(nxt[0][i]) q.push(pii(i, 0));
while(!q.empty())
{
    pii now = q.front(); q.pop();
    int la = SAM_expand(now.first, now.second);
    FOR(i, 0, 25) if(nxt[la][i]) q.push(pii(i, la));
}

构建 SAM 部分:

inline int SAM_expand(int c, int la)
{
    int now = nxt[la][c], p = lk[la];
    len[now] = len[la] + 1;
    for(; ~p && !nxt[p][c]; p = lk[p])
        nxt[p][c] = now;
    if(p == -1) lk[now] = 0;
    else
    {
        int q = nxt[p][c];
        if(len[p] + 1 == len[q]) lk[now] = q;
        else
        {
            int nq = ++tot; lk[nq] = lk[q];
            FOR(i, 0, 25) if(len[nxt[q][i]])
                nxt[nq][i] = nxt[q][i];
            len[nq] = len[p] + 1;
            for(; ~p && nxt[p][c] == q; p = lk[p])
                nxt[p][c] = nq;
            lk[q] = lk[now] = nq;
        }
    }
    return now;
}

细节:

int now = nxt[la][c];

与之前的 now = ++tot 不同,因为字典树上已经有当前状态了,直接拿来用就行

int p = lk[la];

因为 la 已经指向过了 now,因此我们要从 lk[la] 进行遍历

FOR(i, 0, 25) if(len[nxt[q][i]])
    nxt[nq][i] = nxt[q][i];

因为 len 大于当前转态的结点还没加入到 SAM 中,不能将它复制过来


模板题

建好 GSAM 后,我们统计每个结点 \(i\)\(len[i]-len[lk[i]]\) 即可

代码

posted @ 2022-10-10 10:02  zuytong  阅读(48)  评论(0编辑  收藏  举报