字符串

开坑待填。

六个月后,yukari1735 准备开始填坑。

全文大概无图!

Manacher

对每个位置 i=1,2,,n 计算 di 表示以 si 为中心的回文串半径,这里考虑奇数情况,偶数可以简单转化。

这里从 1 开始向右扫,期间维护两个指针 (l,r) 表示当前找到的右端点最靠右的一个回文串,扫到 i 时:

1. 如果 i>r,那么暴力往外扩。

2. 如果 ir,找到 i 在回文串 sl,r 中的镜像 j=l+ri,我们发现,由于 sl,r 的对称性,已经计算过的以 j 为中心的在 sl,r 内的回文串是复制到 i 那里了,摒弃掉超过 sl,r 边界的一块,有 dimin(ri,dj)。剩下的部分暴力扩即可。

别忘了维护 (l,r)

能证明复杂度是线性的。

Hash

哈希函数 f 将字符串 s 映射到整数,可以 O(1) 地判断两个字符串相等。

一般采用多项式哈希方式,即将 s 看作一个 b 进制的大数,为 f(s)=i=1nsi×bni

由于这个数字也比较大,所以我们需要一个模数 p,发生哈希碰撞的概率为 n1p

对于取一个子串的哈希,我们处理出 s 所有前缀的哈希值 fi,子串 sl,r 的哈希值即为 frfl×brl+1

对于同构字符串的一个转化是把第 i 位原来的字符替换为离它上一次出现的距离 iprei,然后就可以哈希判等了。

注意判断第一次出现的字符。

Border

对于一个字符串 s,若 s 的一个前缀 p 同时也是 s 的后缀且 ps,那么称 ps 的一个 border

也是 sborder||=0

记字符串 sborder 集合为 B(s)

Next

对于一个字符串 snexti 定义为 si 前缀 s1,i 中的最长 border 长度(或结尾下标)。形式化一点就是 nexti=max{|x|:xB(s1,i)}

通过不断地跳 next 指针,我们可以遍历 s 的所有 border。因为对于 s 的两个 border x,y (|x|<|y|)x 也是 yborder

所以我们可以得到一个求 next 的方法:设当前已经求出了 s1,inext,我们直接用上面的方法遍历 s1,i 的所有 border,检查是否有 border 可以匹配 si+1

实际上该算法时间复杂度 O(|s|)。证明,我也不会 qwq!

将每个 (i,nexti) 作为边建立一棵树 T,这样的树称为“失配树”。

p 开始通过不断跳 next 可以获得一条从 p 到根的路径,这条路径上的所有的点都对应这一个 s1,pborder,并且下标递减。

s1,p,s1,q 的最长公共 border 就是 p,q 两条路径的第一个交点,也就是 LCA(p,q)

于是可以倍增处理,时间复杂度 O((n+q)logn)

Period

p 为字符串 s 的一个周期,仅当 si=si+p 对于所有 1i|s|p 都成立。

考虑 s 的一个 border x,其对应着一个长度为 |s||x| 的周期。

同样地,一个周期 p 也对应着 s 的一个 border x=s1,|s|p

也即所有的周期与 sborder 存在一一对应的关系,所以我们只需求出 B(s)

KMP

在主串 t 中对单个模式串 s 进行匹配。时间复杂度 O(|t|+|s|)

在匹配到 t 的第 i 位时找到一个最长的串 p,使得其为 t1,i 的后缀,s 的前缀,当 |p|=|s| 时,匹配成功。

首先求出 snext,接着从 t 的起始位置开始匹配,设当前匹配到 tisj,且当前匹配是合法的,那么下一步尝试匹配 ti+1sj+1,若成功则 ii+1,jj+1,继续循环。

否则我们遍历 s1,j 的所有前缀 s1,k,若其也为 tij+1,i 的后缀,则尝试匹配 sk+1ti+1,由于当前有 s1,j=tij+1,i,所以这些前缀 s1,k 都为 s1,jborder,用跳 next 的方法遍历即可。匹配到后令 ii+1,jk+1,继续循环。

j=|s| 时,匹配成功。

KMP Automaton

KMP 算法也可以用自动机来描述,当然,它也是 AC 自动机的基础。

我们在模式串 s 的基础上建立一个确定性有限状态自动机 K=(Q,Σ,δ,q0,qn),其中状态集合 Q 表示匹配到 s 的第几位,Σ 为字符集,初始状态 q0=0,结束状态 qn=|s|

接下来构造转移函数

δ(u,c)={0,u=0su+1cu+1,su+1=cδ(nextu,c),otherwise

其中 uQ,cΣK 接受字符串 t 当且仅当 st 的后缀。

实现时,可以将 t 从起始处开始沿着转移函数走,若可以走到 qn 处则匹配成功。

转移函数可以 O(|s||Σ|) 预处理,匹配时间复杂度 O(|t|)

考虑以 t 串的 KMP 自动机中的状态作为一维状态来进行 DP

fi,j 为字符串长为 i,在 KMP 自动机中的状态为 j 时的最多匹配个数。

转移时枚举当前状态增加一个字符的后继状态,用 fi,j 更新它的后继状态,fi+1,δ(j,c)max{fi+1,δ(j,c),fi,j+[δ(j,c)=|s|]}。注意若 si+1?c 可以选取字符集中的所有元素,否则只能为 si+1

最终答案即为 max{f|s|,j},jQ

时间复杂度 O(|s||t||Σ|),注意到第一维只和上一个有关,所以可以滚动数组将空间优化至 O(|t|)

AhoCorasick Automaton

Aho-Corasick 自动机简称 AC 自动机,可以进行多模式串匹配。

给定若干模式串 s1,s2,,sm,将其插入一棵 Trie T 中,于是 T 中的每个节点 u 都表示了若干个模式串的公共前缀,设这个前缀为 pu。注意,这些模式串的任意一个前缀都被 T 中的一个节点表示。

对每个 uT,定义 failu 指向一个节点 v,满足 pv 是所有前缀中最长的且为 pu 的后缀的串。

我们在 T 的基础上建立一个确定性有限状态自动机 A=(Q,Σ,δ,q0,F),其中状态集合 QT 中的点,Σ 为字符集,初始状态 q0T 的根 r,结束状态集合 F 为所有为模式串结尾的点。

T 中一个节点 u 通过字符 c 边达到的儿子为 son(u,c),空节点为 ,那么转移函数为

δ(u,c)={r,u=rson(u,c)=son(u,c),son(u,c)δ(failu,c),otherwise

A 接受字符串 t 当且仅当某些模式串 sit 的后缀。

KMP 自动机一样,我们仍可以将 t 从起始处沿着转移函数走,若走到一个点 uF 则说明有模式串匹配成功了。

对于计算 fail,和 next 相似,对于一个点 u,我们发现通过不断地跳 fail,仍可以遍历所有满足为任一模式串的前缀且为 pu 的后缀的串 s,并且 |s| 不断减小,于是我们可以直接跳 fail 直到找到一个匹配当前位的串来更新 ufail

总时间复杂度是 O(|si|+|T||Σ|) 的。

朴素的暴力是直接对所有打印出的串建出一个 AC 自动机,对于每个询问暴力跳 fail 暴力匹配,若跳到 x 结尾处则答案加一。

考虑将所有 (failu,u),uT 作为边建立一棵失配树,显然对于一个点 u,它表示的串 puu 的子树中的任意一个点表示的串的后缀,并且任意以 pu 为后缀的串都在 u 的子树中。

询问即为求 y1,i,1i|y||y| 个前缀中有多少以 x 为后缀,注意到这 |y| 个前缀都被 Trie 表示出来了,并且是一个点 uy 的所有祖先,于是问题转化为求 Trie 中的这 |y| 个点有多少点在 fail 树中在 x 的子树中。

可以把询问离线下来挂在 uy 上然后 DFS 整棵 Trie,维护到一个点时它的所有祖先在 fail 树中的点权都是 1,其余点为 0,询问即为子树求和,DFS 序 + 树状数组即可。

时间复杂度 O(n|Σ|+(n+q)logn)

考虑以 AC 自动机中的状态作为一维状态来进行 DP

fi,j 为当前字符串长为 i,在 AC 自动机中的状态为 j 时的最大分值。

转移时枚举当前状态增加一个字符的后继状态,用 fi,j 更新它的后继状态 fi+1,δ(j,c)max{fi+1,δ(j,c),fi,j+w(δ(j,c))} 其中 cΣw(u) 为状态 u 的满足为一个组合技的后缀数量。

然后考虑 w(u) 怎么求,再从失配树的角度看,pu 是所有 u 的子树中节点表示的串的后缀,那么如果 pu 是组合技,子树中所有点的 w+1。所以 w(u)u 在失配树中所有为组合技的祖先数量,DP 一遍即可。

最后答案为 max{fk,q},qQ

时间复杂度 O(k|T||Σ|)

T 及所有修改了一位的 T 都插入 AC 自动机中,答案为 2m()

不合法串数只需要钦定走不到模式串结尾即可。

自动机中点数是 O(n2) 的,时间复杂度 O(n2m)

需要想一个好的转换关系把这些点序列转化为如何操作都不变的且能有足够信息匹配一个元素序列,然后就可以跑 AC 自动机了。

首先记录每个点 C 和前两个点 A,B 形成的夹角 ABC=θ,以及 ABBC,这样的话可以搞定旋转,放缩,平移。

对于翻转操作,可以发现其它的都没变,夹角镜像了。只需要把每个点的这两种转换都插入 AC 自动机里跑匹配即可。

Suffix Array

后缀数组是一些东西:我们将 s 的所有后缀按照字典序排序,sai 表示第 i 名的后缀起始位置,rki 表示第 i 个位置的后缀的排名,大概是反函数这样。

可以倍增来 O(nlogn) 地求一个字符串的后缀数组:倍增一个长度 l,然后只按照所有后缀的前 l 个字符来排序,假如我们已经按照前 l 个字符排好,现在要按照前 2l 个字符排,那么考虑两个后缀 si,nsj,n 的比较,我们首先比较 si,i+lsj,j+l 这部分,这是上一层倍增就排好的,如果相同就比较 si+l+1,i+2lsj+l+1,j+2l 这部分,可以发现这两部分的长度是 l,也是上一层倍增排好的,因此串间比较是 O(1) 的,那么直接排序做得到一个 O(nlog2n) 的做法。

bitset 乱搞匹配

考虑一个模式串上的字符 ti,若主串上有一个和它相同的字符 sj,则 sj+|t|i 有可能成为匹配的终点。

那么我们开 |Σ|01Qa,Qb,,Qz 来存储主串上所有字符的存在位置,然后扫一遍模式串,当扫到 ti 时,我们把 Qti 整体向后平移 |t|i 个位置,此时这个 01 串所有为 1 的位置就是所有可能为匹配终点的位置。

于是我们把所有移动过的 01 串取与和就是答案。

这个东西显然可以用 bitset 搞,做多模式匹配的时间复杂度为 O(|s||ti|ω)

Suffix Automaton

endpos

对于串 s 的任意非空子串 t,记 endpos(t)st 的所有结束位置下标。

对于 s 的任意两个非空子串 t1,t2(|t1||t2),我们有:

  • t1t2 的后缀 endpos(t2)endpos(t1)

  • t1 不是 t2 的后缀 endpos(t2)endpos(t1)=

我们按 endpos 是否相同把所有子串分为若干等价类。

longest(E)shortest(E) 表示等价类 E 中长度最大或最小的串。

len(E)minlen(E) 表示等价类 E 中长度最大或最小的串的长度。

那么在一个等价类 E 中我们有:

  • |t1||t2| t1t2 的后缀。

  • 所有 tE 的大小取值覆盖一个区间 [minlen(E),len(E)]

  • 把一个子串 tE 的所有后缀按照长度降序排序,它们的 |endpos| 单调不减,并且小的被大的包含。

等价类的数量是 O(|s|) 的。

Suffix Link

对于串 s 的任意 endpos 等价类 E,设其中最大的串为 t,则根据上面第五个性质我们可以找到最长的是 t 的后缀且和 t 不属于同一个等价类的串 t,设 t 所属的等价类为 E,则 E 的后缀链接 link(E)=Eminlen(E)=len(link(E))+1

令根 r 为空节点,钦定所有找不到后缀有另外等价类的点的 link=r,那么所有后缀链接构成了一棵树,称为 Parent tree。它也有一些性质:

  • 对于一条从根出发的路径 p(r,E),任意两个 Ep(r,E) 中的串的取值范围 [minlen(E),len(E)] 不交,并且 Fp(r,E)[minlen(F),len(F)]=[0,len(E)]

  • 对于一条从根出发的路径 p(r,E)Fp(r,E)= longeset(E) 的所有后缀。

Suffix Automaton

Suffix Automaton 简称 SAM 或后缀自动机,它接受主串 s 的所有后缀,并且是满足这个条件的最小的自动机。

更重要地,它还具有 s 的所有子串信息,即所有从起始状态到某个状态的路径与 s 中的所有子串存在唯一的一一对应关系。

一个串 s 上的 SAM 是一个确定性有限状态自动机 S=(Q,Σ,δ,q0,F),其中状态集 Qs 中所有的 endpos 等价类,也就是一个等价类对应 S 中的一个状态,q0=

转移比较特殊,考虑一个转移 δ(E,c),我们令 E={t+c|tE},即为所有 E 中的串末尾加上一个字符 c 所构成的集合,若存在另一个等价类 F 使得 EF,那么设置 δ(E,c)=F,否则 δ(E,c)=。可以发现,这样的 F 要么不存在,要么是唯一的。

posted @   yukari1735  阅读(115)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示