字符串笔记

字符串笔记

串串能吃吗?

本文我尝试重新从头开始理解字符串。由此可见,难度梯度可能较大。

发现在谐音替换摩卡串中取得了 \(0+0=0\) 的好成绩。

字符串中很多题目往往可以有多种做法,所以可能会在不同的章节出现多次。在本文中,未加说明的 \(n\) 表示 \(|S|\)

由于内容较多,全部放在同一篇博文中容易造成卡顿,会分为若篇。

基础内容

本部分以概念为主,主要参考 OI-wiki 字符串基础,注意该页面 0-index 和 1-index 的区分。

字符集

字符集,符号为 \(\Sigma\)。是一个全序关系(可视为 \(\le\) )的集合。也就是说,\(\forall x,y \in \Sigma,x \neq y\)\(x<y\)\(x>y\) 有且仅有一个成立。将所有关系 \(x \lt y\) 连一条 \(y \to x\) 的有向边,得到的一定是一张无环的竞赛图。

在算法竞赛中字符集一般为小写字母集,即 \(\Sigma = \{\texttt{a},\texttt{b},\dots,\texttt{z}\},\texttt{a} < \texttt{b} < \dots < \texttt{z}\)

字符集的全序要求是为了字典序的定义。在一些场景中,我们可能会忽略全序性。

字符串

由若干个字符 \(c,c\in \Sigma\) 依次排列的序列,叫做字符串。

和一般的序列一样,字符串可以下标从 \(1\) 开始,亦可以下标从 \(0\) 开始。本文默认从 \(1\) 开始。

\(|S|\) 表示字符串 \(S\) 的长度,即字符串的字符数。\(S_i\) 表示字符串 \(S\) 的第 \(i\) 个字符。

空串是一类特殊的字符串,记为 \(\varepsilon\)\(|\varepsilon|=0\)

字符串的相等定义为两串长度相等且相同位置字符相同。即:\(S=T \Leftrightarrow |S|=|T|,\forall 1 \le k \le |S|,S_k=T_k\)

定义字符串拼接,记作 \(ST\)\(S+T\)\(S,T\) 为字符串)得到一个字符串 \(S_1S_2\dots S_{|S|}T_1T_{2}\dots T_{|T|}\)。记 \(S^n\)\(\begin{matrix}\underbrace{S\cdots S}\\n \text{个} S\end{matrix}\),即将 \(n\) 个字符串依次拼接起来。

子串与子序列

字符串 \(S\) 的子串 \(S_{[l\dots r]}\) 表示由 \(S_l,S_{l+1},\dots,S_{r}\) 顺次拼接而成的字符串。[1]

字符串 \(S\) 的子序列定义为将若干元素提取出来并不改变相对位置形成的序列,即 \(S_{p_1},S_{p_2},\dots,S_{p_k}\) 顺次拼接而成,且 \(1 \le p_1 \lt p_2 \lt \dots \lt p_k \le n\)[1:1]

简单的说就是子串要求在原串中必须连续,子序列不要求。

前缀与后缀

前后缀为两类特殊的子串。前缀为 \(S_{[1,i]}\),后缀为 \(S_{[i,n]}\)[1:2]

字典序

定义字符串的比较关系。

称字符串 \(S<T\) 当且仅当满足以下两个条件之一:

  1. 存在 \(i \in [1,\min \{|S|,|T|\}] \cap \mathbb Z\),满足 \(S_i < T_i\)\(S_{[1,i-1]}=T_{[1,i-1]}\)
  2. \(|S|<|T|\),且 \(S\)\(T\) 的前缀。

字典序的 \(<\) 为严格全序关系。

回文串

回文串为从前往后和从后往前读完全一样的串。形式化地,\(S_{i}=S_{|S|-i+1}\)

一些常用的缩写

LCP(Longest Common Prefix):指最长公共前缀。类似的有 LCS,最长公共后缀。

字符串哈希

字符串哈希是一类相当通用的算法。[2]

模板:P3370 【模板】字符串哈希 - 洛谷,更强的数据:U461211 字符串 Hash(数据加强)- 洛谷

可以发现,比较两个字符串是否相等是 \(\mathcal O(|S|)\) 的。

而哈希的思想就是将很大的集合范围映射到一个较小的值域范围。所以该映射一般为单射,基本不可能做到双射。

什么叫“小的值域范围”,对于哈希表而言为空间允许的范围,而对于字符串哈希主要是为了比较两字符串是否相等,在 64/32 位整数下就可以了。

一般字符串哈希采用多项式哈希。即

\[H(S)= \sum _{i=1} ^{|S|} S_i \times b ^{|S|-i} \bmod P \]

其中式中的 \(S_i\) 常是用 \(S_i\) 在字典序中的排名,且空出 \(0\) 不用,避免 \(\texttt{a}\)\(\texttt{aa}\) 无法区分。

\(b\)\(P\) 这两个参数的取值则影响哈希的正确性,常见的 \(P\)\(\sim 10 ^9\) 的大质数或者直接取 \(2^{64}\) 自然溢出,但均可以 Hack(Hash Killer I/II/Extra)。

所以实践中常使用双哈希,但也不是万能的:Hash Killer Phantasm

但是,未知 \(b\)\(P\) 的双哈希暂时未被 Hack,Hash Killer III【Open Problem】

在实践中由于随机大质数有些困难,随机底数即可。在 CodeForces 的赛制下这很重要。

而在其它问题中,我们往往需要求一个子串的哈希值。可以发现,哈希值是可差分的,这就说明,我们在求出前缀哈希值后可以通过差分求出任意子串的哈希值。具体地:

\[H(S_{[l,r]})=H(S_{[1,r]})-H(S_{[1,l-1]}) \times b ^{r-l+1} \]

KMP 与 Border 理论

有限状态自动机

参考资料


  1. 在定义中没有说明空串和整个字符串是否为子串/子序列/前缀/后缀,这是因为在不同的时候使用不同的定义会更加方便。 ↩︎ ↩︎ ↩︎

  2. 很多问题都可以通过字符串哈希得到一个复杂度相同或复杂度略高(比如多一个 \(\log\))的做法。 ↩︎

posted @ 2026-04-24 21:57  MZMTab  阅读(24)  评论(0)    收藏  举报