字符串基础学习笔记

前言

开这个坑的目的是巩固一下字符串的基础内容,毕竟自己对这一块的接触还不是很多。

其实字符串算法最大的特点就是 最大化利用已经求出的信息,几乎所有算法都是基于这句话的。

一些定义:

  • lcp(i,j) 为以 i 开头的后缀和以 j 开头的后缀的最长公共前缀。
  • σ 为字符集。
  • si,js 下标在 [i,j] 之间的字串。
  • |S| 为字符串 S 的长度。
  • Bmax(Si,j) 为字符串 Si,j 的最长 border。

Hash

概述

Hash 就是把一个字符串映射成一个数字的过程,常见的构造函数类似于:

hash(S)=i=1|S|Si×Base|S|imodp

Base 可以取一个小质数,p 可以用一个大质数。如果觉得不保险还可以用双哈希,即选取两个 Basep

例题 1:「CTSC2014」企鹅QQ

题面

题意:

给定 n 个长度为 L 的字符串,问有多少对字符串只有一位不同。

数据范围:1n300001L200

对每个前缀和后缀分别哈希,枚举哪一位不同然后统计。

最小表示法

概述

模板

题意:给定一个字符串 S,求一个 i[1,|S|],使得 Si,|S|+S1,i1 的字典序最小。

维护两个指针 ij,表示当前比较的两个循环表示的起点。

暴力求出 k=lcp(i,j),然后比较 Si+kSj+k,不妨假设 Si+k<Sj+k,那么肯定 [j,j+k] 范围内的所有下标都不可能成为最小循环表示的起始位置(因为以 j+x 起始的循环表示字典序一定大于以 i+x 起始的循环表示)。令 jj+k+1,继续暴力做这个过程。

注意以上下标都是在 mod n 意义下的。

每个指针只会扫一遍字符串,所以时间复杂度为 O(|S|)

代码

Manacher

概述

Manacher 算法可以求出以每个下标为中心的最长的 长度为奇数 的回文子串。

为了求出长度为偶数的回文子串,我们可以在原串相邻两个字符中间插入一个不属于 σ 的字符。

pi 为以 i 为中心的最长回文半径,当前最大的 i+pi1(即已经被某个点为中心的回文串所覆盖到的最右端点) 为 mr,这个最大的 imid

考虑如何利用上述信息求得一个新的 pi。若 i>r,那么暴力扩展求;否则有 ir,根据回文的对称性,可以得出 pi=min(mri+1,p2×midi)

为什么这样是对的?考虑在 [midpmid+1,mid+pmid1] 的范围内,2×midii 对称,所以以 2×midi 为中心的回文串同时也是以 i 为中心的回文串。而这个性质只能在当前范围内满足,所以还要和 mri+1min

代码

模板

例题 2:「THUPC2018」绿绿和串串

题面

长度为 n 的串肯定满足条件。

对于每一个 i,如果存在以它为中心的回文串包括 n 这个位置,或者 存在以它为中心且以 1 开头的回文串且 2i1 符合题目条件,那么 i 就是一个合法答案。

Z 算法 / exKMP

概述

Z-algorithm 可以求出所有的 zi=lcp(1,i)

类似 Manacher,每次维护最靠右的 r=l+zl1 和这个最大的 l,可以发现 zimin(ri+1,zil+1)。然后和 Manacher 一样暴力扩展并更新 l,r 即可。

利用 Z-algorithm 解决字符串匹配问题:将两个字符串拼接在一起,中间用一个不属于 σ 的字符隔开。

代码

模板

例题 3:「NOIP2020」字符串匹配

题面

题意:

T 组数据,每次给定一个字符串 S,求 S=(AB)iC 的方案数,其中 F(A)F(C)F(S) 表示字符串 S 中出现奇数次的字符的数量。

两种方案不同当且仅当拆分出的 ABC 中有至少一个字符串不同。

数据范围:1T51|S|220

首先可以求出每个后缀 / 前缀出现奇数次的字符数量,枚举 ABAB 的出现次数,用哈希暴力判断是否合法,这样就可以知道 F(C) 了,然后用一个树状数组统计合法的 A 的个数,时间复杂度 O(T|S|log|S|log26),可以得到 84 分。

考虑优化这个过程。先求出 Sz 函数,枚举 AB 的长度 i,那么可以扩展的次数就是 zi+1i+1

如何处理 F(A)F(C) 的限制?我们把扩展的次数按奇偶性讨论。

  • AB 出现了奇数次,即 k 次,那么我们只看第 1AB,则 [i+1,ki] 中的字符出现次数的奇偶性并没有被改变(因为 k1 为偶数),所以 F(C)=F(Si+1,|S|),可以直接算出。
  • AB 出现了 偶数次,那么 F(C)=F(S),因为这个前缀出现的字符都出现了偶数次,不会改变奇偶性。

对于以上两种情况分别用树状数组统计合法的 A 之后加起来即可。

代码:https://paste.ubuntu.com/p/2WSwnkhbVm/

例题 4:「CF526D」Om Nom and Necklace

题面

同样枚举 AB 的长度 i1,那么 AB 要出现奇数次的充要条件就是 zik(i1),此时能贡献到的区间就是 [k(i1),min(ki,zi+i1)],差分后前缀和即可。

例题 5:「CF432D」Prefixes and Suffixes

题面

求出 Sz 函数。枚举 S 的一个长度不超过 |S| 的后缀 [i,|S|],如果 zi=|S|i+1,说明这个后缀是一个 border。

怎么统计一个 border 的出现次数?可以发现对于每个位置 i,以它开头的字符串对长度为 [1,zi] 的前缀有一次贡献,差分后做一遍后缀和即可。

代码:https://paste.ubuntu.com/p/xzCPwbhBTF/

KMP

概述

KMP 一般用来解决字符串匹配问题。

KMP 的核心在于一个 nxt 数组,nxti 存储的是 [1,i] 的最长 border(即前缀和后缀相等)。

维护两个指针 i,j,假设我们已经知道 sij+1,it1,j 匹配,那么肯定就有 sij+nxtj+1,it1,nxtj 也能匹配。因为 sij+1,ij+nxtj=sinxtj+1,i=t1,nxtj=tjnxtj+1,j,所以当前串 s 的前 nxtj 个字符和串 t 的后 nxtj 个字符相等,当 si+1tj+1 失配的时候就可以直接 jnxtj

如何求得 nxti?这个可以通过 t 串自己和自己匹配求得。具体来说,假设我们已经知道了 nxt1i1,想要求 nxti,那么 t1,i 的最长 border 肯定是由 t1,i1 的一个 border(不一定是最长)在后面加上 ti 得到。那么我们从 j=nxti1 开始匹配,如果失配就 jnxtj,直到 tj+1=ti。如果 j=0 就需要判断一下 t1 是否等于 ti

注意:如果题目中对于 nxt 的定义给出了若干限制,那么我们必须要先不考虑限制求出 nxt,然后再去处理限制,否则可能会导致求出的 nxt 错误。

代码

模板

例题 6:「NOI2014」动物园

题面

先求出 nxti,顺便计算出 cnti 表示 i 要跳多少次 nxt 才能变成 0,即 i 的 border 数量。

在求 numi 的时候,先把指针 p 跳到 i2 的最大位置上,然后就有 numi=cntp+[p>0][p>0] 的意思是 [1,p] 这个 border 没有在 cntp 中统计到。

代码:https://paste.ubuntu.com/p/xw83TsXhbH/

例题 7:「POI2006」PAL-Palindromes

题面

由于给出的串都是回文串,所以有结论:若 a+b 是一个回文串,当且仅当它们的最短回文整周期串相同。

充分性显然。必要性考虑反证法,具体留给读者作为练习。

有了结论之后就可以先用 KMP 求出 nxt 数组,若 nnxtn|nxtn 说明 nxtn 就是这个字符串的最短回文整周期串长度,否则就是它本身。开个哈希表把这些串的最短回文整周期串存下来,直接计算贡献即可。

代码:https://paste.ubuntu.com/p/d2GZS9GHMw/

例题 8:「POI2012」前后缀 Prefixuffix

题面

如果两个串满足「循环等价」,那么肯定它们分别形如 ABBA。不妨假设前缀形如 AB,后缀形如 BA

所有满足条件的 A 其实就是这个串的 border,暴力跳 next 即可求出。

问题变成了怎么求一个子串 Si,ni+1 的最长 border。

这个可以考虑增量法构造。假设我们已经求出了 Si+1,ni 的最长 border,那么肯定有 Bmax(Si,ni+1)Bmax(Si+1,ni)+2,这是因为 Si+1,ni 的 border 可以通过 Si,ni+1 的 border 去掉头和尾的字符得到。那么从最中间的字符开始暴力往两边扩展即可。

代码:https://loj.ac/s/1412375

例题 9:「POI2005」SZA-Template

题面

思路巧妙.jpg

dpi 表示要覆盖 [1,i] 所需要的印章长度的最小值。

考虑 dpi 实际上只有两种取值:idpnxti,因为不可能没有覆盖 nxti 就覆盖了 i,不然最后会没办法填到 i

问题变成在什么时候 dpi 能取到 dpnxti。其实很简单,考虑印印章的过程,在印 i 的时候肯定起点在 [inxti+1,i] 中,所以只有在满足 inxtiji1,dpj=dpnxti 的时候 dpi=dpnxti

代码就很好写了:https://pastebin.ubuntu.com/p/zzMsTwB6Y3/

KMP 自动机

概述

KMP 自动机是一种 确定有限状态自动机

实质就是在 KMP 求出的 nxt 数组的基础上,额外求出 transi,j 表示在 i 之后接上字符 j 会转移到什么状态,即:

transi,j={i+1si+1=j0si+1ji=0transnxti,jsi+1ji>0

和 KMP 的转移类似,应该很好理解。

代码

例题 10:「HNOI2008」GT考试

题面

对输入的字符串建出 KMP 自动机。

gi,j 表示现在匹配的长度为 i,加入一个字符后匹配长度变成 j 的方案数,fi,j 表示已经填了 i 个字符,匹配长度为 j 的方案数。转移可以用矩阵乘法加速。

代码:https://paste.ubuntu.com/p/Q4ZfQ4bqQT/

border 与周期理论

定理

rS 的周期当且仅当 S 有长度为 |S|r 的 border。

Weak Periodicity Lemma

内容

pq 是字符串 S 的周期,且 p+q|S|,则 gcd(p,q)S 的周期。

证明

不妨设 p<qd=qp

  • i>p,则有 si=sip=sip+q=si+d
  • 否则 ip,有 si=si+q=si+qp=si+d

这样我们就证明了 d 是字符串 S 的一个周期。

根据更相减损术,最终能得到 gcd(p,q)S 的一个周期。

引理

S 所有不超过 |S|2 的周期都是其最短周期的倍数。

或者等价的,S 所有长度不小于 |S|2 的 border 长度构成等差数列。

不难通过 Weak Periodicity Lemma 得出。

定理

S 的所有 border 长度(周期)构成 O(logn) 个值域不交的等差数列。

Periodicity Lemma

内容

p​,q​​ 是 S​​ 的周期,且 p+qgcd(p,q)|S|​​,则 gcd(p,q)​​ 也是 S​​ 的周期。

失配树

概述

在 KMP 算法中,观察到 nxti<i,那么如果我们连一条边 (nxti,i),就可以得到一棵树,我们称之为 失配树

性质:失配树上每个节点的祖先都是它的一个 border。

因此,如果我们要求两个前缀 S1,pS1,q 的最长公共 border 的长度,可以直接在失配树上求出 p,q 两点的 LCA。注意如果 pq 具有 祖先-后代 关系,那么应该输出深度较低的那个点的父亲,因为一个串的 border 不能是它自己。

代码

模板

例题 11:「BOI2009」Radio Transmission 无线传输

题面

根据周期理论那一套,一个字符串的最短周期就是 nnxtn

输出 nnxtn 即可。

例题 12:「POI2006」OKR-Periods of Words

题面

根据失配树的定义,一个字符串的最长周期就是它在根节点之下深度最小的点,在失配树上倍增跳即可。

代码:https://loj.ac/s/1415484

posted @   csxsi  阅读(121)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示