字符串基础
KMP
考试实在忘记了的话,可以拿字符串哈希实现。
\(Next_i\) 代表的是以 \(i\) 为终点的后缀和以 \(Next_i\) 为终点的前缀相等。
注意 \(Next_1\) 的值为 \(0\),若为 \(1\) 则成环。
经典应用: P4391 [BOI2009] Radio Transmission 无线传输
结论:若 \(i \bmod (i-next_i)=0\) 且$\dfrac{i}{i-next_i}>1 $,则 \(S\) 有长度为 \(i-next_i\) 的循环节。
CF1575H Holiday Wall Ornaments
多模板串约束可以用 ACAM,对于单模式串我们同样可以建立 KMP 自动机来模拟赛匹配过程的变化,在自动机上 dp 即可,时间复杂度 \(O(n^3)\)。
border理论
- 字符串 \(s\) 所有不小于 \(\dfrac{\lvert s \rvert}{2}\)的 \(border\) 构成等差数列。
- 可以把字符串分成 $\log \lvert s \rvert $ 段,每一段的 \(border\) 构成等差数列。
失配树
\(KMP \to fail\) 树。
P5829 【模板】失配树
两个前缀的 border 就是他们在 fail 树上的 lca。
QOJ5256. Insertions
一道很巧妙的字符串匹配题,并不需要用什么高级后缀结构。
题目给的三个小问启发我们其实应该是转化为求出在每个位置插入的匹配次数,而非单纯找一些最大的。
记 \(S,T,P\) 的长度分别为 \(n,m,p\)。
有一些很显然的匹配形式,假设我们在 \(S(i,i+1)\) 处插入字符串 \(T\),那么 \(S[1:i],S[i+1:n],T[1:m]\) 都是可以直接和 \(P\) 匹配的,这个可以预处理。
还有一些比较难处理,我们分成两类,一种是 \(S[l:i]+T[1,r]\) 或 \(T[l,m]+S[i+1,r]\),另一种是 \(S[l:i]+T+S[i+1,r]\)。
对于模式串 \(P\) 建立 fail 树。
对于第一种匹配两个类别是对称的,不妨讨论 \(S[l:i]+T[1,r]\),也就是一段前缀拼后缀。我们用 \(T\) 的前缀 \(T[1:j]\) 匹配 \(P\) 的后缀 \(P[p-j+1,p]\)(Z 函数可以统计),对于符合要求的匹配点 \(p-j+1\) 我们在 fail 树上标记一下这个点的上一个位置 \(p-j\)(代表如果 \(S\) 能匹配到 \(p-j\),就能和后面的连一起产生贡献) 。然后我们只需要在 \(S\) 上满足前缀要求即可,可以维护当前 \(S[1:i]\) 的后缀与 \(P\) 的前缀匹配的最长位置,所有满足 \(S[1:i]\) 的后缀与 \(P\) 的前缀匹配的位置都是上述的最长位置跳 fail 得来的,于是也就是当前点到根节点路径上标记点的个数。
对于第二类匹配类似地,我们找到所有的 \((l,r)\) 满足 \(T=P[l:r]\),同时必须满足 \(l\neq 1,r\neq n\),否则会会和前面的统计产生重复贡献,标记二元组 \((l-1,r+1)\)。建立 \(P\) 的正反串 fail 树,然后就变成了一个两颗树上数两个点到根节点公共点个数的 ds 题,注意此处的公共点表示可以构成被标记的二元组 \((l,r)\)。做法类似于 IOI2018 werewolf 是对于标记点对 \((p,q)\) 改为 \((dfn_p,dfn_q)\),注意两个 \(dfn\) 不同,分别表示在两颗树上 dfs 序。我们用扫描线来扫描第一颗树的 dfs 序,线段树维护第二颗树的 dfs 序,每次进行区间修改,单点查询即可。
\(n,m,p\) 视为同阶,时间复杂度 \(O(n\log n)\)。
Manacher
首先为了避免分类讨论,我们应该统一奇偶,在所有串每个空隙(包括首尾)之间插入一个无关字符,这样子回文中心就一定是某个字符了,而非空隙。
可以得到原串中最长回文子串的长度等于新串最长回文子串的半径减一,即 \(d=R-1\)。不是最长的未必满足这个性质。
我们维护当前右端点最远的对称中心 \(c\),设其半径为 \(r_c\),右端点为 \(R=c+r_c-1\)。设我们当前在考虑位置 \(i<R\),那么 \(i\) 关于 \(c\) 的对称点就是 \(2c-i\),
观察这张图,我们发现在 \(2c-i\) 地方的小区域对称正好也可以通过 \(c\) 点对称到 \(i\) 点来,但是注意如果 \(2c-i\) 的对称左端点越过了 \(L\) 就意味着多出来的那一部分无法通过 \(c\) 点传递我们也无法确认。于是 \(r_i \gets \min(r_{2c-i},R-i+1)\)。如果 \(r_i\) 取的是前者,那就意味着后面已经无法再继续匹配了,直接终止,如果取的是后者,我们暴力继续往外扩展即可。
可以发现最远右端点的移动的是 \(O(n)\) 的。所以时间复杂度线性。
这也可以证明一个字符串的本质不同回文子串个数不超过 \(n\) 个。
写代码时千万别忘记在开头加上分隔字符。
P4555 [国家集训队] 最长双回文串
我们只要求出 \(x_i\) 与 \(y_i\) 表示原串中以 \(i\) 开头和结尾的最长回文子串的长度即可,然后 \(\max x_i+y_{i+1}\) 拼接即可。如果求 \(x_i\) 呢?首先原串的每个位置 \(i\),在加入额外字符之后会变成 \(2\times i\),于是我们只要在新串的偶数位置更新即可,自己模拟几种情况我们可以发现 \(x_{i/2}=\max\limits_{c+r_c-1\ge i}\{i-c+1\}\)。除以 \(2\) 是因为要映射回原串,为了满足 \(c+r_c-1\ge i\) 的要求,我们在每次 \(i+r_i-1 > R\) (不能取等) 的时候暴力用 \(i\) 作为中心更新 \((R,i+r_i-1]\) 即可。
UVA11475 Extend to Palindrome
有点构造题的感觉。找到最小的 \(l\),满足 \([l,n]\) 为回文,然后直接将 \(s[1,l-1]\) 翻转后放到末尾即可。
exKMP
\(z_i\) 表示字符串 \(s\) 与 \(suf_i\) 的最长匹配长度,其中 \(z_1=n\)。
对于 \(z\) 的求法与思想 Manacher 类似,称 \([i,i+z_i-1]\) 为匹配段,我们只要维护最靠右的匹配段即可,记为 \([l,r]\)。假如当前考虑到了 \(i\),对于 \(i>r\),我们暴力匹配,对于 \(i\le r\),我们可以通过之前求出的结论得到 \(s[i,r]=s[i-l+1,r-l+1]\),这不正好和 \(z_{i-l+1}\) 有关吗,于是 \(z_i \gets \min(r-i+1,z_{i-l+1})\)。
和 Manacher 同理就是右端点的移动是线性的,所以复杂度为 \(O(n)\)。
CF432D Prefixes and Suffixes
首先用 KMP 匹配可以求出所有前缀等于后缀的地方。
然后这其实是一个关于前缀计数的东西,我们要统计出某些前缀在串中出现了几次,我们发现较短的前缀被包含在较大的前缀中,如果位置 \(i\) 的 \(Z\) 函数为 \(z_i\),那么 \([1,z_i]\) 的前缀都出现过,直接差分即可。
PKUSC2023 Border
面对这种类似单点修改全局查询的东西,且各个操作独立的题,一定要注意信息变化量很小,且只允许一个信息与原始的偏差。一个偏差意味着如果可以在替换情况下构成 border,那么我们找到第一个不符合条件的位置,然后它的前后必然都是符合条件的,并且该位置可以被符合条件的替换。
形式化地来说,可能产生贡献的位置必须满足之前 \(s[i,n]\) 和 \(s[1,n-i+1]\) 之间最多有一个位置不同。可以想到用 Z 函数。对于我们对于 \(i\) 找到 \(i+z_i\) 这个位置代表是第一个不同的位置。此时有两种可能,一种是 \(s_{i+z_i}\to t_{i+z_i}\) 之后符合条件,另一种是 \(s_{z_i+1}\to t_{z_i+1}\) 之后符合条件。我们对于两种分别判断即可。
于是本题的答案统计就以下几个部分:
- \(s_i=t_i\),那 \(ans_i\) 就是 \(s\) 的最长 border。
- \(s_i\neq t_i\),最基本的就是找到一个最长 border,其长度 \(l\) 满足 \(l<i<n-l+1\),这一部分可以对于序列前后两半分别扫描一遍统计。还有一种就是上文提到的替换后的贡献。
Trie
01 Trie 就不在字符串这里记录了。
P7537 [COCI2016-2017#4] Rima
先考虑暴力,就是两两算一下能不能押韵,然后建边,以每个点为起点跑一遍最长链。
我们可以在字典树上解决这个问题,我们建反串,仔细思考一下这个问题,其实就是在从一个节点开始在字典树上行走,每次可以到父亲或者同父的兄弟,问最多能到达多少节点,树形 dp 一下即可。
AC自动机
\(Tire\) 树 \(\to\) \(Tire\) 图。
构建过程:先建立 Trie 树,然后把与根相连的第一个字符入队,同时 \(f\) 设为 \(0\),防止自环。然后开始扩展,如果当前 \(r\) 点没有字符 \(c\),那么 \(ch_{r,c}\gets ch_{f_r,c}\)。否则入队,然后 \(f_u\gets ch_{f_r,c}\),同时更新 \(last\) 数组,如果 \(f\) 位置有尾节点,那就是 \(f\) 了,否则就是 \(last_f\)。
bfs 性质保证了长度小的串先被遍历到,所以 \(f\) 与 \(last\) 数组一定可以保证连到。
CF710F String Set Queries
三种做法。
二进制分组
二进制分组,发现每一个字符串最多被重构 \(\log n\) 次,于是时间复杂度为 \(O(n\log n)\)。我们发现到答案的可减性,于是可以建立两个 AC 自动机分别负责添加和删除然后最后的答案就是添加组减去删除组。这里注意写法,直接创立两个个 AC 自动机的结构体,数组都在结构体里面开,每次调用不同结构体即可这样代码难度就减了很多。
实现细节,用每次当 \(sz_{top}=sz_{top-1}\) 的时候进行合并,合并的过程就是对应节点 \(val\) 相加。为了方便维护,我们在 getfail 的时候不能随便连 ch 了,必须新建立一个 sh 来连。
一种很典的做法
神奇的思路,我们设 \(\sum \lvert S_i \rvert=m\),于是\(\lvert S_i \rvert\) 的不同个数为 \(O(\sqrt n)\) 级别,每次枚举 \(T\) 中长度为 \(\lvert S_i \rvert\) 的字串,然后哈希判断即可。
根号分治
根号分治,较短串用字典树维护,较长串用 KMP 维护。
P2444 [POI2000] 病毒
直接在 AC 自动机上不碰到尾节点搜索,如果有环就可以无限绕着环走,这样是安全的。
注意有向图判环不能 \(0~1\) 标记,应该是 \(0~1~2\) 标记。
P2414 [NOI2011] 阿狸的打字机
我们发现其实就是对每个串建立一个 KMP,多个串的 KMP 自然就想到了 AC 自动机。
每次查询对于 \(y\) 链上的每个子串暴力跳 fail 的话太慢了,可以离线存下所有询问对于每个 \(y\) 链一起跳。
其实反过来考虑,这本质就是一个 \(x\) 的 fail 树的子树对于 \(y\) 求和问题。我们发现 \(y\) 是 AC 自动机上的一条链。于是可以顺着 AC 自动机的边走,进入累加,离开减去。这样所有时刻被累加的都是一条链上的贡献。
注意上述沿着 AC 自动机的边,指的是未创立 fail 之前的边,否则会成环。
DP
其实 AC 自动机上 dp 一般就是出现某种些字符串可以得分或者某些字符串禁止出现。前者是 P5319 [BJOI2019] 奥术神杖,后者是 ZROI2979.数数。
bitset 完成字符串匹配
基本思路用 bitset 移位维护模式串的每个终止位置。
具体来说用 \(c_s\) 表示字符 \(s\) 在文本串中出现位置,其中 \(c\) 为一个 bitset,出现则该位为 \(1\)。
对于模式串 \(t\) 的每一位 \(t_i\) 都将 \(c_{t_i}\) 左移 \(m-i\) 位和 \(ans\) 按位与。
CF914F Substrings in a String
法一:bitset 匹配。注意细节如果模式串长大于区间长度可能会减出负数,又因为 cout() 的类型是 unsigned int 所以需要我们取 int,然后和 \(0\) 比一下大小。
法二:这其实是一个 kmp 的过程。记住这种询问多个字符串的问题,询问的总长是一定的!!每次都重构一次 kmp 显然会被长度很小的查询复杂度卡掉。这启发我们进行根号分治,大于阀值 \(B\) 的查询我们直接进行 kmp 匹配,这种串不会超过 \(\frac{n}{B}\) 种。对于长度小于阀值 \(B\) 的查询,我们维护从每个下标开始的不超过 \(B\) 的每个哈希值,查询的时候暴力匹配即可。
法三:SAM 分块。
CF963D Frequency of String
法一:bitset 匹配
法二:考虑暴力每次暴力扫描时间复杂度为 \(O(\lvert S\rvert\sum\lvert m_i\rvert)\) ,也就是说用哈希匹配字符串,然后找到所有 \(m_i\) 出现位置,用滑动窗口扫一遍就行了。可以根号分治,对于长度大于 \(B\) 的串,直接执行上述操作。对于长度小于 \(B\) 的串,离线下来,从 \(s\) 的每一个位置开始
同理维护即可。这里需要用到一个结论保证复杂度: \(m\) 个不同的(长度之和为 \(n\) 的)串在同阶长度的中的文本串中的 endpos 集合大小为 \(O(n \sqrt{n})\)。