「学习笔记」后缀自动机
推荐博客 link
子串问题可以考虑
但是如果要维护不同子串个数类似的问题的话, 的 的量到了
那么后缀自动机作为压缩的 就应运而生了,主要是合并了大量没用的节点
定义 & 性质
首先是一个子串的 表示当前子串的在大串里面结尾下标的集合
每个 会对应若干个子串,这些子串长度必然连续
如果在这些子串里面最长的前面加入两个不同的字符,可以把当前集合分成两个没有交集的集合(可能有元素没有被分到这两个集合里面)
那么可以得到 的数量级是 的,像线段树式的分割方法可以得到最大的数目
如果考虑把母集作为分割出来小集合的父亲,就可以得到一个树,这里是
这里有一个性质,设 为当前 的最长的字符串的长度, 为最短的字符串的长度,那么有
证明就是子集合是父亲加字符,所以就好理解了
针对 的定义和性质可以得到以下的性质:
的每个点代表的是一个 ,从源点通过加边到达这个点的字符串要保证它的 是这个集合
但是可能并不会把这个 到底是几显式地表示出来
到达 的任意字符串是到达 的后缀
因为 ,这里和平常的字符串维护不太一样
颠覆了一点认知,但是仔细思考上面说的 的性质还是成立的
那么后缀自动机的构建就是使它满足这个条件
建立
首先每次新加入一个字符得到的字符串所有的后缀的 都会改变
那么遍历所有原串的后缀,如果没有 的出边,就连上(这样保证了 有维护子串个数的功能)
如果出现有且满足 ,那么按照 来理解,就直接把 设为 就行了
如果出现了 但是不满足 ,那么需要新建一个节点
也就是分割
然后把 的信息给到 接着把所有的前面满足相应的 指向
inline void extend(int x){
int tmp=las,np=las=++tot; len[np]=len[tmp]+1;
while(tmp&&!ch[tmp][x]) ch[tmp][x]=np,tmp=fa[tmp];
if(!tmp) return fa[np]=1,void();
int q=ch[tmp][x];
if(len[q]==len[tmp]+1) return fa[np]=q,void();
int clone=++tot; fa[clone]=fa[q]; fa[q]=fa[np]=clone; copy(clone,q);
len[clone]=len[tmp]+1;
while(tmp&&ch[tmp][x]==q) ch[tmp][x]=clone,tmp=fa[tmp];
return ;
}
应用
判断子串:和 自动机类似
求在原串所有子串中(相同的算或者不算一个)字典序第 大
每个串出现的次数等价于建出来后缀树之后对应的 对应的点的子树的点全和
得到后缀树上面的子树点权值,然后考虑从这个字母出去的子串有几个的时候要考虑在 上面处理
输出考虑树上二分即可
求本质不同的子串个数
考虑到 上根节点到任意一个点都是一个子串,且其本身是个
所以拓扑 一下就好了
如果在线的话,那就考虑每个点的贡献吧: 即
两串 :
其实就是后缀树上点的 的 (然后“差异”那个题就可以考虑每条边的长度是 然后贡献法一下了)
关于后缀树:
这个比较就是每个点的父亲向点建边,建成了一棵树,就是后缀树
最小表示法:
这个比较简单了,把原串复制一遍,然后直接在后缀自动机上面跑,能往小的点上跑,就跑,没有后继节点就跳
求最长公共子串的长度:
用其中一个字符串建后缀自动机,然后把其它的子串放在上面匹配
记录下来每个点匹配的最长长度,然后在每个点上面取记录下来 的 吧
最后的答案就是所有点 的
(很绕,但是很好理解)
维护子串排名
每个后缀树上的点维护的是一个后缀,那么我们翻转之后就能得到前缀
先算 大小表示这个点一共多少个串
然后把所有的出边都标号,标号就是
这样每个点在后缀树上排序之后再 得到每个点的
对于每个 倍增出来对应的点之后直接 即可
相关复杂度:
建后缀自动机的复杂度和后缀自动机的点数是 的
后缀树和 树的节点数量级是 的
遍历后缀自动机对应的 是 的
广义 SAM
支持了多串匹配的功能
一个例题是 [USACO17DEC]Standing Out from the Herd P
考虑按照存 ,但是空间会炸,应该用大量stl也是能写的
其实发现只用维护每个点是不是被经过了两次
所以标记即可
或者是 字符串
考虑修改两个字符串的代价
那就搞出来广义 ,把所有的子串在对应节点上打上标记
然后建后缀树,现在问题转化成了:树上有一些点,求让这些点两两匹配后每对 的深度之和最大
贪心发现每两个在它最先能匹配的 匹配会优,那么甚至都可以不用
记得判断 的时候直接赋成 就完事了
例题
这篇博客主要放一些线段树维护 的题目
NOI2018 你的名字
首先考虑一个 的做法是对于每个 建 然后跑可达性统计,然后两个串一起跑,如果这个点在 里面能跑到 里面没有就加上可达的点
貌似也没啥优化的空间,那么换一个思路,往 后缀树 上面靠
直接统计还是不太行,那么正难则反,用总的 上的减掉公共子串
具体就是求一下 表示 到 的前缀有多长可以在 上面作为一段后缀出现
如果如果不考虑存在重复的情况,就把 放到 上面跑,每次不行了就跳
(这里深刻一个观点:后缀树上面点的深度基本上是没啥用的,含义就可以轻松得证)
因为 里面的都是 的后缀,又要选最大值,所以每跳一下,长度减成现在点的
那么这就得到了一个 的做法
接着考虑怎么做不是全串的情况,先求所有的 集合
(如果对后缀树的相关熟练的话,其实这步是很好说的)
然后考虑对于 的限制怎么写
然而并不能每次暴力重建,但是每次只是找儿子,跳父亲,找当前点在 内的最大
第一个其实对应的是 有没有在 之间有点
第二个是跳父亲,因为父亲的 是母集,随便跳
最后一个比较恶心,想清楚了是
是指在 内的最大的 下标,这个可以用线段树求出来
还是一样的跑就行了
HAOI2018 字串覆盖
显然的贪心就是尽可能取位置靠前的点
那么问题转化成了维护另一个子串的 在这个子串的出现位置
按照古老的套路就是把一个建出来自动机另一个跑匹配
如果 那么答案就是
反之考虑当前的点的在 中所有的
查询的话复杂度是
数据大的时候显然能过,但是数据小的时候会完蛋
那么考虑重新维护一个 表示当前字符串是 ,后面的 的字符串的位置,同时记录 为所有的 之和
每个询问倍增跳出来所有的位置和求和即可
CF700E
的暴力就是枚举两个子串是不是能满足关系
然后 最长链,显然这东西是铁废物
在 上那么转移必然是从父亲向儿子进行的
那么问题就变成了如何维护出现两次的限制
本质上出现了两次就是说这个子串的 有至少两个点在上一个子串的所在区间 中出现了
线段树合并求 是
直接做是 的
如何进行快速完整的转移
其实本质上可以在后缀树上倍增,每次暴力跳来找到最大的满足条件的祖先,然后转移即可
貌似可以证明深度更深的点的答案不会劣于深度浅的点
不过倍增复杂度不行,那么考虑记录转移点即可
十二省联考2019 字符串问题
因为不会 所以想想这题目怎么用 写
预处理显然,倍增跳后缀树维护出来每个点代表的 ,复杂度
如果能连接那么下一个串得是上一个串的限制的前缀
前缀就是反串的后缀
然后建反串的自动机,考虑在这上面得到的 的限制就可以变成 向 的子树里面建边了
那么因为是个树,所以有 序,所以线段树优化建图
具体而言就是每个 点往区间连边,点带权就完事了
写的时候要特别注意
但是这样的话会发现如果 和某个 倍增到同一个点上了
如果 共点的时候两个 就是完全可以转移的
那么我原来直接把 放到 的方法就不奏效了
所以排序然后暴力扫,做前缀优化建图
具体就是按照长度把当前的点的 排序,把树上的每个点拆成 和
父亲 连给 , 连给满足条件的这点的 或者这个点自己的 , 只能往外连
然后拓扑即可
Border的四种求法
因为一定有一个 是 ,所以这题转出来就变得清晰了许多
对应出来这个点的链,那么问题就是找到树上的 的这个 ,同时需要的是最大的
因为上面的点的 肯定比下面的大,但是 要比下面的小,所以貌似没有单调性
然后只会暴力跳父亲,所以写了 (为啥长度不满足就不跳了不给分)
拓展上面的做法,按照上面的思路可以先倍增到想对应的位置,省去无效的查询
不过还是完全不会,貌似在线是没法支持的,所以试试离线的思路
关键结论: 可以理解成一个最大的 满足
然后扔到 树上面就是两个点的 最大
那么每个询问的 必然在 的链上面
所以变成了一个很好的数据结构题,学到了很多:
先对 树链剖分,每个询问的挂到所有的重链的底部:
把所有的询问放到 向上的重链底下,最后统一遍历所有重链来对所有的答案进行更新
可能的 只会在这条链上面,但是构成 的另外一个前缀点就可能在链顶的子树里面
遍历重链的时候考虑维护当前点作为 ,把所有其轻子树里面的前缀点放到一个线段树里面
这个线段树下表为 的点存
那么对于挂在这个点上的询问进行线段树上的二分即可
归总,其实这是我真正意义上第一道做的树剖或者 题吧
倒和字符串没有太大关系了
最后:数据水得过分了,过不了小数据点的代码能过大数据(所以我就数据点分治过掉了这题)
其实答案统计当然是要枚举两个点,一个是在维护的 上的,另一个是在原来的线段数合并得到的那个上面查非完全子段的答案
所以一开始写挂了
Codeforces1063F
观察一下发现是个最长下降的模型,所以考虑
直接硬上 很不优美,所以翻转一下,变成最长上升
设 为到反转后的第 个点的最长上升序列的长度
显然存在最优的方式使得 ,那么 也就表示了最后一个串的长度
那么求出来当前的 的串的表示,看看是不是有可转移的点即可
现在是 的复杂度(我没写不知道能不能过掉)
不过由 建立的时候的写法就能观察出来 是单调不降的
那么加入 的时候维护一个指针
这样还是 的复杂度
考虑这样一个结论, ,貌似是 的
那么就做完了
Luogu5576
一开始的思路是建出来广义 然后每次询问跑所有点,判断是不是 中的点都在这个点上
然后想到离线,把询问挂到什么东西上,对于每个点线段树合并的时候维护一下
考虑 和启发式合并来规避空间的限制,然后就写了两天的 区间合并
接着得到了 的好成绩
最后还是学到了分治的想法:(真的想不太到)
考虑正经 维护 的题目的做法,那么考虑设当前有一个
长度 的是短串,反之是长串
如果一个区间里面全是短(或长)串就 即可
要短串中位置中间的一个作为区间的标准串建 , 的求后缀, 维护前缀的最大值
那么考虑所有跨过 的询问直接在 中查询最大值即可
这个思路确实是开阔了视野,其实也不太难写出来233
2021-04-05 最小表示
考虑 来维护这样的子串,最后还是len[i]-len[fa[i]]
来统计答案
对于题目中的最小表示的定义,显然在其当前第一次出现的时候会不一样
下述视角均为从后往前,也就是下一次为通常意义上的左边一次
那么我们考虑其上一次出现之后到其的位置,维护每个位置最后在 上出现的位置 和其下一次在原串中出现的位置
每次扩展 ,注意只对当前点所对应的后缀自动机上的点的 加一
(要写那种广义自动机形式的东西)
最后遍历后缀树,对于有 大小的节点进行答案统计
后缀自动机上的节点不超过 ,证明考虑每种字符向前统计不超过 次
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律