后缀自动机的构建和应用

其实是在瞎口胡

参考:Meatherm 的奇妙博客

SAM 的线性构造

SAM 的每个节点都表示一个 endpos 等价类,由于 SAM 和 parent tree 共用节点,所以我们要做的就是在这些节点上添加边使其能接受所有的后缀. 将一个节点的后缀边连向另一个节点表示这个节点的 endpos 等价类中所有串在后面加上一个字符会到达另一个 endpos 等价类. parent tree 上的父亲边将一个节点的 endpos 等价类划分开来,成为互不相交的新等价类,即跳父亲就是前往 endpos 等价类中以当前等价类最短串删去前面的一个字符为最长串的等价类.

我们在后缀自动机的每个节点上存储这个节点的等价类中的最长串长度 \(len_i\),parent tree 上的父亲 \(fa_i\) 和后缀边 \(ch_c\) 连向的节点.

考虑增量法构建 SAM,即在前面所有的字符 \(s_{1\sim i-1}\) 都已经加入 SAM 之后考虑新增 \(s_i\) 新增的边. 我们从上次加入后的节点 \(lst\) 开始,不难发现,由于新增的字符改变了所有原串的后缀,我们就要给新节点连上后缀边并且找到父亲,也就是说我们要找出 \(s_{1\sim i}\) 后缀的所有等价类. 事实上,沿着 \(lst\) 的父亲一直向上遍历到根,有结论:

  • 记途中经过的所有等价类的最长串为 \(f_1,f_2,\cdots,f_k\),那么我们就会发现,对于所有和 \(f_j\) 在同一个等价类的后缀 \(f'_j\),\(f'_j+s_i\) 也在加入字符 \(s_i\) 后的 SAM 的同一个等价类中.

    并且由于 \(f'j+s_i\) 总是出现在 \(s{x+1}=s_i\) 且 \(x\in\text{endpos}(f'_j)\) 的位置 \(x\),所以访问到的 endpos 等价类是不会遗漏的.

所以我们可以从 \(p=lst\) 开始不断向上遍历 \(fa_p\),因为后缀的特殊结构决定了上个节点一定是包含之前所有后缀的,就可以根据这个节点是否有 \(ch_{c=s_i}\) 这条边来分讨连边情况.

设新增节点为 \(nw\),根节点 \(rt=1\).

Case 1

如果 \(p\) 没有出边 \(ch_c\),说明当前的 SAM 上缺少 \(x+s_i\) 的信息,需要令出边 \(ch_c=nw\),此时需要继续遍历.

那么为什么从 \(lst\) 开始遍历 \(fa_p\) 找到的串就是我们要更新的信息呢?考虑跳 \(fa_p\) 其实是在等价类前面删字符,这样可以遍历之前所有的后缀所在的等价类,再把新增字符 \(c\) 对后缀带来的影响表示出来就好了.

当然,如果 \(p\) 已经到达根节点,就可以直接令 \(fa_{nw}=rt\) 并停止遍历,因为此时必然是第一次加入 \(c\) 这个字符.

Case 2

如果 \(p\) 存在一条出边 \(q=ch_c\),其实我们不能直接让这个等价类直接作为 \(fa_{nw}\). 因为如果 \(x+s_i\) 不是 \(q\) 的最长串,此时 \(q\) 中只有一部分串是 \(x+s_i\) 的后缀.

注意 \(p\) 和 \(q\) 是父子关系,所以如果 \(len_p+1\neq len_q\) 其中必然因为 \(c\) 的加入缺失了一部分子串信息,这些缺少的信息是无法通过修改原图增加的,因为原图保证了最小性. 我们必须新增节点把 \(q\) 的信息拆一部分出来来添加后缀边维护新的子串并且调整父子关系,由于此时的 \(p\) 和 \(q\) 不再符合父子的要求. 记这个新节点为 \(nq\),令 \(len_{nq}=len_{p}+1\),且 \(nq\) 继承所有 \(q\) 的出边,此时 \(nq\) 与 \(q\),\(nq\) 与 \(nw\) 符合父子关系,有 \(fa_q=fa_{nw}=nq\). 然后需要修改所有 \(p\) 原有 \(c\) 出边指向 \(q\) 的为 \(nq\),否则后缀边是不连续的.

如果 \(len_p+1=len_q\),说明此时所有字串信息都没有缺失,直接令 \(fa_{nw}=q\) 即可.

以上两种子情况都不需要继续遍历 \(fa_p\),因为已经确保操作后维护了所有的性质.

实现

非常的简短,非常的好写啊

cpp 复制代码
struct node{int ch[26], fa, len;} t[maxn << 1];
int lst = 1, tott = 1;
void insert(int c) {
	int p = lst, nw = lst = ++tott; t[nw].len = t[p].len + 1;
	for(; p && !t[p].ch[c]; p = t[p].fa) t[p].ch[c] = nw;
	if(!p) {t[nw].fa = 1, ans += t[nw].len - t[t[nw].fa].len; return;}
	int q = t[p].ch[c]; if(t[q].len == t[p].len + 1) t[nw].fa = q;
	else {
		int nq = ++tott; t[nq] = t[q];
		t[nq].len = t[p].len + 1; t[q].fa = t[nw].fa = nq;
		for(; p && t[p].ch[c] == q; p = t[p].fa) t[p].ch[c] = nq;
	}
	return;
}

应用

根据 SAM 的强大性质可以解决不少串串题,但是有一些进阶应用还要结合其它数据结构.

最小表示法

发现这就是找在环上长度为 \(n\) 的字典序最小的字串,直接倍长插入到 SAM 中然后从根节点开始不断走字典序最小的字符,走 \(n\) 步得到的串即为答案.

求本质不同字串个数

根据 SAM 定义,每个节点上压缩的串个数之和就是 \(ans\). 由于 endpos 等价类中的串长度是连续的,所以每个节点 \(nw\) 的贡献就是 \(len_{nw}-len_{fa_{nw}}\). 在插入字符时在线统计一下就好了.

求字串出现次数

对于某个子串,从根节点开始走找到对应节点,那么这个节点包括子树内所有节点都出现过这个字串,且一定不重不漏. dfs 数子树大小即可.

求两个(多个)串的最长公共子串

对于两个串的情形,可以对其中一个串建 SAM,另一个串在 SAM 上匹配

相关推荐
吃鱼不卡次4 天前
视觉大模型专栏导航
大模型·sam·cv
伊织code5 天前
SAM 2 (Segment Anything ):图像与视频通用分割模型
sam·图像·视频·模型·segment·anything·分隔
大家都说我身材好5 天前
如何优化字符串替换:四种实现方案对比与性能分析
java·字符串
carpell18 天前
【双指针法】:这么常用的你怎么能不知道
python·链表·字符串·数组·双指针法
Tisfy19 天前
LeetCode 2843.统计对称整数的数目:字符串数字转换
算法·leetcode·字符串·题解
知来者逆19 天前
YOLO目标检测应用——基于 YOLOv8目标检测和 SAM 零样本分割实现指定目标分割
yolo·目标检测·计算机视觉·图像分割·sam·yolov8
G皮T25 天前
【Python Cookbook】字符串和文本(五):递归下降分析器
数据结构·python·正则表达式·字符串·编译原理·词法分析·语法解析
longyitongxue1 个月前
洛谷 - B4276 [蓝桥杯青少年组国赛 2023] 八进制回文平方数 - 题解
字符串·转进制
ゞ 正在缓冲99%…1 个月前
leetcode76.最小覆盖子串
java·算法·leetcode·字符串·双指针·滑动窗口