当前位置:首页 > 技术 > 正文内容

广义后缀自动机的构建与应用

访客 技术 2026年6月11日 1

广义后缀自动机(Generalized Suffix Automaton, GSA)是一种高效处理多字符串子串问题的数据结构,能够在线性时间内构建并支持多种查询操作。其核心思想是将多个字符串的子串统一建模为一个确定有限自动机(DFA),从而实现对所有字符串中任意子串的快速识别与统计。

构建方式

离线方法:基于 Trie 树的 BFS 遍历

该方法适用于已知所有输入字符串的情况。首先建立这些字符串的公共前缀树(Trie),然后通过广度优先搜索遍历 Trie 节点。对于每个节点,将其对应的字符插入到 SAM 中,并利用父节点的对应状态进行转移。

  • trie[u][c] 表示 Trie 树中节点 u 的字符 c 子节点。
  • t_c[cur] 为当前节点在 Trie 中的父节点。
  • pos[cur] 记录该节点在 SAM 中所对应的节点编号。
  • 插入函数 ins(c, lst) 返回新状态的编号,逻辑与标准 SAM 一致。
inline void build_gsam() {
    queue<int> q;
    for (int c = 0; c < 26; ++c)
        if (trie[1][c]) q.push(trie[1][c]);
    pos[1] = 1;

    while (!q.empty()) {
        int cur = q.front(); q.pop();
        pos[cur] = ins(t_c[cur], pos[t_fa[cur]]);
        for (int c = 0; c < 26; ++c)
            if (trie[cur][c]) q.push(trie[cur][c]);
    }
}

此方法结构清晰,适合离线处理,但需额外存储 Trie 结构。

在线方法:逐串插入 + 特判优化

在线方式无需预先构造 Trie,可逐个添加字符串。关键在于两个特判以避免重复创建或错误转移:

  1. 完美匹配判断 若当前状态 lst 已有边 c 指向长度恰好为 len[lst] + 1 的节点,则直接返回该节点,避免冗余操作。

  2. 分裂节点处理 当发现需分裂节点时,引入新节点 nq 作为中间状态,继承原节点 q 的转移信息,并更新父子关系。若当前路径起始于 lst,则说明新节点应作为主节点返回。

inline int insert(int c, int lst) {
    int &next = son[lst][c];
    if (next && len[next] == len[lst] + 1)
        return next;

    int np = ++tot;
    len[np] = len[lst] + 1;
    int p = lst;

    // 延展路径直到无法继续
    while (p && !son[p][c])
        son[p][c] = np, p = fa[p];

    if (!p) {
        fa[np] = 1;
        return np;
    }

    int q = son[p][c];
    if (len[q] == len[p] + 1) {
        fa[np] = q;
        return np;
    }

    bool is_split = (p == lst);
    int nq = ++tot;
    len[nq] = len[p] + 1;
    fa[nq] = fa[q];
    fa[q] = fa[np] = nq;
    memcpy(son[nq], son[q], sizeof(son[q]));

    while (p && son[p][c] == q)
        son[p][c] = nq, p = fa[p];

    return is_split ? nq : np;
}

在线做法空间更紧凑,代码简洁,常用于竞赛环境。


常见应用

判断某串是否为任意给定字符串的子串

构建广义 SAM 后,可在自动机上模拟匹配过程,从初始状态出发,按字符依次跳转。若最终能到达某个有效状态,则该串是某个原始串的子串。

统计多串中本质不同的子串总数

利用 SAM 的性质:每个节点贡献的唯一子串数为 len[i] - len[fa[i]]。总和即为答案。

long long distinct_substrings() {
    long long ans = 0;
    for (int i = 2; i <= tot; ++i)
        ans += len[i] - len[fa[i]];
    return ans;
}

维护每串的结束位置集合(endpos)

在每次插入过程中,标记"当前串结尾"的节点 siz 为 1。若发生分裂,则将 npsiz 值转移至 nq。最后通过拓扑排序结合线段树合并,还原每个节点的 endpos 集合。

求解多串最长公共子串

目标是寻找被所有字符串共同包含的最长子串。算法步骤如下:

  • 构建广义 SAM 并维护每个节点的 endpos 集合大小(记录属于多少个原始串)。
  • 遍历所有节点,若某节点的 endpos.size() == 总串数,则该节点代表的子串是公共子串。
  • 用最大 len[i] 更新答案。

典型例题:SP1812 LCS2 - Longest Common Substring II


实际挑战与调试建议

部分题目如「洛谷 P3473」涉及复杂边界条件,例如多串交替、空串、重复模式等,容易导致状态分裂异常或 siz 值错位。建议在调试时打印关键节点的 len, fa, son[]siz 信息,辅助定位问题。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。