「学习笔记」后缀自动机

推荐博客 link

子串问题可以考虑 trie

但是如果要维护不同子串个数类似的问题的话,trieDAG 的量到了 O(n2)

那么后缀自动机作为压缩的 trie 就应运而生了,主要是合并了大量没用的节点

定义 & 性质

首先是一个子串的 {endpos} 表示当前子串的在大串里面结尾下标的集合

每个 endpos 会对应若干个子串,这些子串长度必然连续

如果在这些子串里面最长的前面加入两个不同的字符,可以把当前集合分成两个没有交集的集合(可能有元素没有被分到这两个集合里面)

那么可以得到 {endpos} 的数量级是 O(n) 的,像线段树式的分割方法可以得到最大的数目

如果考虑把母集作为分割出来小集合的父亲,就可以得到一个树,这里是 Parent Tree

这里有一个性质,设 mx(x) 为当前 {x} 的最长的字符串的长度,mn(x) 为最短的字符串的长度,那么有 mx(fax)+1=mn(x)

证明就是子集合是父亲加字符,所以就好理解了


针对 endpos 的定义和性质可以得到以下的性质:

(1) SAM 的每个点代表的是一个 endpos,从源点通过加边到达这个点的字符串要保证它的 endpos 是这个集合

但是可能并不会把这个 endpos 到底是几显式地表示出来

(2) 到达 fax 的任意字符串是到达 x 的后缀

因为 xendposfaxendpos,这里和平常的字符串维护不太一样

颠覆了一点认知,但是仔细思考上面说的 endpos 的性质还是成立的

那么后缀自动机的构建就是使它满足这个条件

建立

首先每次新加入一个字符得到的字符串所有的后缀的 endpos 都会改变

那么遍历所有原串的后缀,如果没有 x 的出边,就连上(这样保证了 sam 有维护子串个数的功能)

如果出现有且满足 lenp=lentmp+1 ,那么按照 endpos 来理解,就直接把 fa[np] 设为 q 就行了

如果出现了 q 但是不满足 lenp=lentmp+1 ,那么需要新建一个节点 clone

也就是分割 endpos

然后把 q 的信息给到 clone 接着把所有的前面满足相应的 endpos 指向 clone

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 ;
}

应用

判断子串:和 AC 自动机类似


求在原串所有子串中(相同的算或者不算一个)字典序第 i

每个串出现的次数等价于建出来后缀树之后对应的 endpos 对应的点的子树的点全和

dp 得到后缀树上面的子树点权值,然后考虑从这个字母出去的子串有几个的时候要考虑在 DAG 上面处理

输出考虑树上二分即可


求本质不同的子串个数

考虑到 sam 上根节点到任意一个点都是一个子串,且其本身是个 Dag

所以拓扑 dp 一下就好了

如果在线的话,那就考虑每个点的贡献吧:maximini+1maximax[fa[i]]


两串 LCP :

其实就是后缀树上点的 lcalen(然后“差异”那个题就可以考虑每条边的长度是 lenilen[fa[i]] 然后贡献法一下了)

关于后缀树:

这个比较就是每个点的父亲向点建边,建成了一棵树,就是后缀树


最小表示法:

这个比较简单了,把原串复制一遍,然后直接在后缀自动机上面跑,能往小的点上跑,就跑,没有后继节点就跳 fail


求最长公共子串的长度:

用其中一个字符串建后缀自动机,然后把其它的子串放在上面匹配

记录下来每个点匹配的最长长度,然后在每个点上面取记录下来 maxmin

最后的答案就是所有点 minmax

(很绕,但是很好理解)


维护子串排名

每个后缀树上的点维护的是一个后缀,那么我们翻转之后就能得到前缀

先算 endpos 大小表示这个点一共多少个串

然后把所有的出边都标号,标号就是 s[pos[x]+len[fa[x]]]

这样每个点在后缀树上排序之后再 dfs 得到每个点的 rk

对于每个 [l,r] 倍增出来对应的点之后直接 rk[now]+len[now](rl+1)+1 即可


相关复杂度:

(1) 建后缀自动机的复杂度和后缀自动机的点数是 O(n)

(2) 后缀树和 parent 树的节点数量级是 O(n)

(3) 遍历后缀自动机对应的 DAGO(n2)

广义 SAM

支持了多串匹配的功能

一个例题是 [USACO17DEC]Standing Out from the Herd P

考虑按照存 sz[id][x] ,但是空间会炸,应该用大量stl也是能写的

其实发现只用维护每个点是不是被经过了两次

所以标记即可

或者是 SNOI2020 字符串

考虑修改两个字符串的代价 kdep[lca]

那就搞出来广义 SAM,把所有的子串在对应节点上打上标记

然后建后缀树,现在问题转化成了:树上有一些点,求让这些点两两匹配后每对 lca 的深度之和最大

贪心发现每两个在它最先能匹配的 lca 匹配会优,那么甚至都可以不用 dp

记得判断 len[x]>k 的时候直接赋成 k 就完事了


例题

这篇博客主要放一些线段树维护 endpos 的题目

NOI2018 你的名字

首先考虑一个 n2 的做法是对于每个 TSAM 然后跑可达性统计,然后两个串一起跑,如果这个点在 T 里面能跑到 S 里面没有就加上可达的点

貌似也没啥优化的空间,那么换一个思路,往 后缀树 上面靠

直接统计还是不太行,那么正难则反,用总的 T 上的减掉公共子串

具体就是求一下 l[i] 表示 Ti 的前缀有多长可以在 S 上面作为一段后缀出现

如果如果不考虑存在重复的情况,就把 T 放到 S 上面跑,每次不行了就跳 fa

(这里深刻一个观点:后缀树上面点的深度基本上是没啥用的,含义就可以轻松得证)

因为 fa 里面的都是 x 的后缀,又要选最大值,所以每跳一下,长度减成现在点的 len

那么这就得到了一个 68pts 的做法


接着考虑怎么做不是全串的情况,先求所有的 endpos 集合

(如果对后缀树的相关熟练的话,其实这步是很好说的)

然后考虑对于 [lr] 的限制怎么写

然而并不能每次暴力重建,但是每次只是找儿子,跳父亲,找当前点在 [l,r] 内的最大 len

第一个其实对应的是 endpos 有没有在 [l,r] 之间有点

第二个是跳父亲,因为父亲的 endpos 是母集,随便跳

最后一个比较恶心,想清楚了是 min(len[i],nowmax(now,pos)+1)

pos 是指在 [l,r] 内的最大的 endpos 下标,这个可以用线段树求出来

还是一样的跑就行了

HAOI2018 字串覆盖

显然的贪心就是尽可能取位置靠前的点

那么问题转化成了维护另一个子串的 endpos 在这个子串的出现位置

按照古老的套路就是把一个建出来自动机另一个跑匹配

如果 led[r]<rl+1 那么答案就是 0

反之考虑当前的点的在 [tl,tr] 中所有的 endpos

查询的话复杂度是 len1len2×logn

数据大的时候显然能过,但是数据小的时候会完蛋

那么考虑重新维护一个 flen,i,j 表示当前字符串是 S[i,i+len1] ,后面的 2j 的字符串的位置,同时记录 sumlen,i,j 为所有的 pos 之和

每个询问倍增跳出来所有的位置和求和即可

CF700E

O(n4) 的暴力就是枚举两个子串是不是能满足关系

然后 dp 最长链,显然这东西是铁废物

SAM 上那么转移必然是从父亲向儿子进行的

那么问题就变成了如何维护出现两次的限制

本质上出现了两次就是说这个子串的 endpos 有至少两个点在上一个子串的所在区间 [st,ed] 中出现了

线段树合并求 endpostrivial

直接做是 O((2n)2logn)

如何进行快速完整的转移

其实本质上可以在后缀树上倍增,每次暴力跳来找到最大的满足条件的祖先,然后转移即可

貌似可以证明深度更深的点的答案不会劣于深度浅的点

不过倍增复杂度不行,那么考虑记录转移点即可

十二省联考2019 字符串问题

因为不会 SA 所以想想这题目怎么用 SAM

预处理显然,倍增跳后缀树维护出来每个点代表的 ai or bi,复杂度 O((na+nb)log|S|)

如果能连接那么下一个串得是上一个串的限制的前缀

前缀就是反串的后缀

然后建反串的自动机,考虑在这上面得到的 ab 的限制就可以变成 ab 的子树里面建边了

那么因为是个树,所以有 dfn 序,所以线段树优化建图

具体而言就是每个 a 点往区间连边,点带权就完事了

写的时候要特别注意 i or dfn[i]

但是这样的话会发现如果 ai 和某个 bj 倍增到同一个点上了

如果 a and b 共点的时候两个 lenalenb 就是完全可以转移的

那么我原来直接把 a 放到 faid 的方法就不奏效了

所以排序然后暴力扫,做前缀优化建图

具体就是按照长度把当前的点的 alen,blen 排序,把树上的每个点拆成 num1num2

父亲 num1 连给 bb 连给满足条件的这点的 a 或者这个点自己的 num2a 只能往外连

然后拓扑即可

Border的四种求法

因为一定有一个 endposr ,所以这题转出来就变得清晰了许多

对应出来这个点的链,那么问题就是找到树上的 max{endpos[l,r]},len[p]endposl+1 的这个 p,同时需要的是最大的 endpos

因为上面的点的 endpos 肯定比下面的大,但是 len 要比下面的小,所以貌似没有单调性

然后只会暴力跳父亲,所以写了 50pts(为啥长度不满足就不跳了不给分)

拓展上面的做法,按照上面的思路可以先倍增到想对应的位置,省去无效的查询

不过还是完全不会,貌似在线是没法支持的,所以试试离线的思路

关键结论:Border 可以理解成一个最大的 i 满足 lcs(i,r)il+1

然后扔到 parent 树上面就是两个点的 len[lca] 最大

那么每个询问的 lca 必然在 ed[r]root 的链上面

所以变成了一个很好的数据结构题,学到了很多:

先对 parent 树链剖分,每个询问的挂到所有的重链的底部:

把所有的询问放到 r 向上的重链底下,最后统一遍历所有重链来对所有的答案进行更新

可能的 lca 只会在这条链上面,但是构成 lca 的另外一个前缀点就可能在链顶的子树里面

遍历重链的时候考虑维护当前点作为 lca ,把所有其轻子树里面的前缀点放到一个线段树里面

这个线段树下表为 i 的点存 ilen(lca(ed[i],now))

那么对于挂在这个点上的询问进行线段树上的二分即可

归总,其实这是我真正意义上第一道做的树剖或者 DS 题吧

倒和字符串没有太大关系了

最后:数据水得过分了,过不了小数据点的代码能过大数据(所以我就数据点分治过掉了这题)

upd: 其实答案统计当然是要枚举两个点,一个是在维护的 SGT 上的,另一个是在原来的线段数合并得到的那个上面查非完全子段的答案

所以一开始写挂了

Codeforces1063F

观察一下发现是个最长下降的模型,所以考虑 DP

直接硬上 SAM 很不优美,所以翻转一下,变成最长上升

dp[i] 为到反转后的第 i 个点的最长上升序列的长度

显然存在最优的方式使得 |ti||ti+1| ,那么 dp[i] 也就表示了最后一个串的长度

那么求出来当前的 [idpi+1,i1],[idpi+2,i] 的串的表示,看看是不是有可转移的点即可

现在是 O(nlog2n) 的复杂度(我没写不知道能不能过掉)

不过由 SAM 建立的时候的写法就能观察出来 ilen[pos] 是单调不降的

那么加入 dp[i] 的时候维护一个指针

这样还是 Θ(nlog2n) 的复杂度

考虑这样一个结论, dp[i]dp[i1]+1,貌似是 trivial

那么就做完了

Luogu5576

一开始的思路是建出来广义 SAM 然后每次询问跑所有点,判断是不是 [l,r] 中的点都在这个点上

然后想到离线,把询问挂到什么东西上,对于每个点线段树合并的时候维护一下

考虑 set 和启发式合并来规避空间的限制,然后就写了两天的 区间合并

接着得到了 40pts 的好成绩


最后还是学到了分治的想法:(真的想不太到)

考虑正经 SAM 维护 lcp 的题目的做法,那么考虑设当前有一个 x

长度  x 的是短串,反之是长串

如果一个区间里面全是短(或长)串就 ++/x 即可

要短串中位置中间的一个作为区间的标准串建 SAM[mid,r] 的求后缀,[l,mid] 维护前缀的最大值

那么考虑所有跨过 mid 的询问直接在 [l,r] 中查询最大值即可

这个思路确实是开阔了视野,其实也不太难写出来233

2021-04-05 最小表示

考虑 SAM 来维护这样的子串,最后还是len[i]-len[fa[i]] 来统计答案

对于题目中的最小表示的定义,显然在其当前第一次出现的时候会不一样

下述视角均为从后往前,也就是下一次为通常意义上的左边一次

那么我们考虑其上一次出现之后到其的位置,维护每个位置最后在 SAM 上出现的位置 pos[i] 和其下一次在原串中出现的位置 dst[i]

每次扩展 pos[i],dst[i] ,注意只对当前点所对应的后缀自动机上的点的 size 加一

(要写那种广义自动机形式的东西)

最后遍历后缀树,对于有 size 大小的节点进行答案统计

后缀自动机上的节点不超过 Θ(nm),证明考虑每种字符向前统计不超过 n

posted @   没学完四大礼包不改名  阅读(216)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示