广义后缀自动机 学习笔记

广义后缀自动机 学习笔记


概述

广义后缀自动机(General Suffix Automaton)是将后缀自动机整合到字典树中来解决对于多个字符串的子串问题。

定义

字符串集合 \(\set{s}\) 的广义后缀自动机(GSAM)是一个接受 \(\set{s}\) 的所有字符串的所有后缀的最小 DFA(确定性有限自动机或确定性有限状态机)。

记号约定

  • \(s\):目标字符串。
  • \(sta_0\):初始状态。
  • \(\operatorname{tr}(u,c)\):状态 \(u\) 接受了字符 \(c\) 后转移到的状态。
  • \(\operatorname{endpos}(t)\):一个集合,表示 \(s\) 中子串 \(t\) 的结束位置。
  • \(\operatorname{link}(u)\):状态 \(u\) 对应的后缀链接(与 AC 自动机中的失配指针 \(\operatorname{fail}\) 相似)。
  • \(\operatorname{longest}(u)\):状态 \(u\) 对应的字符串集合中最长子串。
  • \(\operatorname{link}(u)\):状态 \(u\) 对应的字符串集合中最长子串长度。

常见的伪广义后缀自动机

  1. 通过用特殊符号将多个串直接连接后,再建立 SAM。

    很显然这会造成匹配到无效子串、产生无效状态的问题。

  2. 对每个串,重复在同一个 SAM 上进行建立,每次建立前,将 last 指针置零。

    对可能会造成对于同一个 \(\operatorname{\operatorname{endpos}(t)}\) 状态在 SAM 中有多个节点,导致出错。

离线构造

SAM Drawer (officeyutong.github.io)

用一般 SAM 的思想进行延伸,我们可以得到 GSAM 的离线构造方法。

构建 Trie

首先第一步是构建一棵 Trie 树,只要把集合 \(\set{s}\) 中所有串都插入即可。

BFS 建立 GSAM

然后对于建完的 Trie 考虑 BFS 建立 GSAM,在 BFS 时记录当前字符和父节点在 GSAM 上的 \(lst\) 状态,然后 BFS 时类似 SAM 构造的 Extend 函数加入字符。

但是这里 GSAM 的 Extend 与 SAM 的 Extend 有所不同:

  • 不创建新状态,因为我们已经离线建好了 Trie 树,于是直接在 Trie 树上跳到对应子节点即可。

    还有一个细节问题,在第一次跳 \(\operatorname{link}(u)\) 更新 \(\operatorname{tr}(p,c)\) 时,由于原节点的 \(\operatorname{tr}(lst,c)\) 已经设为了 \(lst'\),所以我们要先跳一步,防止直接终止。

    int p(lst);
    len(lst=tr[p][c])=len(p)+1,p=pa(p);
    while(p&&!tr[p][c])tr[p][c]=lst,p=pa(p);
    
  • 也是由于我们提前建好了 \operatorname{tr}(u,c)ie 树,那么在克隆节点的时候,可能会把一些 \(\operatorname{link}\) 比它大的给当成 \(\operatorname{tr}(u,c)\),对于这种情况,我们在克隆的时候要判断时一下 \(\operatorname{link}\)

    FOR(i,0,C-1)tr[o][i]=len(tr[q][i])?tr[q][i]:0;
    

那么 Extend 中其余的就没有区别了。

int Extend(int lst,const int c) {
	int p(lst);
	len(lst=tr[p][c])=len(p)+1,p=pa(p);
	while(p&&!tr[p][c])tr[p][c]=lst,p=pa(p);
	if(!p)return pa(lst)=1,lst;
	int q(tr[p][c]);
	if(len(q)==len(p)+1)return pa(lst)=q,lst;
	int o(++tot);
	len(o)=len(p)+1,pa(o)=pa(q),pa(q)=pa(lst)=o;
	FOR(i,0,C-1)tr[o][i]=len(tr[q][i])?tr[q][i]:0;
	while(p&&tr[p][c]==q)tr[p][c]=o,p=pa(p);
	return lst;
}

考虑 BFS,不过这比较简单:

void Build() {
	queue<pair<int,int> > q;
	FOR(i,0,C-1)if(tr[1][i])q.push({1,i});
	while(!q.empty()) {
		pair<int,int> u(q.front());
		q.pop();
		int lst(Extend(u.first,u.second));
		FOR(i,0,C-1)if(tr[lst][i])q.push({lst,i});
	}
}

在线构造

引用原文链接:辰星凌的博客QAQ (cnblogs.com)

由于离线的一般已经够用,所以我们暂时搁置。

复杂度

复杂度方面,较难证明的都在 SAM 处,而这些可以在 SAM 相关的 blog 中找到,这里不在赘述。

剩下的 BFS 和 Trie 树都是线性的不必多说。

性质

在后缀自动机上的性质绝大部分均可在广义后缀自动机上生效。

——广义后缀自动机 - OI Wiki (oi-wiki.org)

没了……

应用

GSAM 在应用方面肯定是不如 SAM 多的,比较多串的题目比较难出,而且 GSAM 结构限制较大。

本质不同子串数

例题:

  1. P6139 【模板】广义后缀自动机(广义 SAM) - 洛谷 (luogu.com.cn)
  2. P3346 [ZJOI2015] 诸神眷顾的幻想乡 - 洛谷 (luogu.com.cn)

最基本的,与 SAM 相同,是纯模版题。

\[\sum \operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i)) \\ \]

求每个询问串是几个模版串的子串

例题:SP8093 JZPGYZ - Sevenk Love Oimaster - 洛谷 (luogu.com.cn)

建 GSAM,然后暴力跳 \(\operatorname{link}\) 更新即可,复杂度是 \(O(n\sqrt{n})\),证明:算法记录 002: SAM 上的根号暴力 - WWW~~~ - 博客园 (cnblogs.com)(根号分治)。

或者配线段树合并可以到 \(O(n\log_2{n})\)

字符串间的公共子串数量

例题:P3181 [HAOI2016] 找相同字符 - 洛谷 (luogu.com.cn)

那么建出 GSAM,统计 \(siz_{i,0/1}\) 表示有多少个子串在此处,建完后拓扑累加,最后得到答案:

\[\sum siz_{i,0}\times siz_{i,1}\times[\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i))] \\ \]

单用 SAM 也可以做。

多个字符串间的最长公共子串长度和数量

例题:SP1812 LCS2 - Longest Common Subs\operatorname{tr}(u,c)ing II - 洛谷 (luogu.com.cn)(长度)。

其实这个 SAM 也可以做,而且不难,复杂度也较低,为 \(O(\sum{|S|})\)

这里的 GSAM 做法其实要好理解一点,我们如果只求长度,记一个 \(vis_{p,i}\) 数组表示是否各个串在各个节点处都有子串,那么全部建完然后再拓扑排序或一遍,最后查询每个串都有的点求最大长度即可。

如果还要求数量,那么 \(vis\) 改为 \(cnt\),定义变为出现了几次,然后拓扑排序的时候累加即可,与上一个应用相同。

\(n\) 表示串的总数,只查询长度的话,复杂度可以做到 \(O(\frac{n\sum{|S|}}w)\),否则是 \(O(n\sum{|S|})\) 的,没有比 SAM 的做法好。

配合数据结构

例题:CF666E Forensic Examination - 洛谷 (luogu.com.cn)

线段树合并+GSAM。


posted @ 2025-08-21 14:16  Add_Catalyst  阅读(14)  评论(0)    收藏  举报