字符串
字符串
字符串,从笔者入门时就以进入,但真正的字符串远没有那么简单,如果读者硬要抬杠,那么不好意思,笔者将拿出这道题:
CF1200E
这道题十分简单,所以我们放到后面讲解
约定
字符串是一个有字符创造的序列,本文用加粗表示字符串 如: s = $\mathbf{Luqiuyu} $
字符集是是可以拥有排序关系的一种有字符组成的集合。
若没有歧义,本文将用 |S| 代表长度,且经常用 n 来表示
子串是指字符串中一段连续段,本文用 \(s[l,r]\) 表示,其中 l 是他的左端点, r 是右端点
空串用 $\varepsilon $ 表示,其中, \(|\varepsilon | = 0\)
反串是翻转原串的字符串,用 \(s^R\) 表示,其中 s 是原串
回文串是指 \(s = s^R\) 的回文串,特别的, $\varepsilon $ 也是回文串
拼接.....不用介绍了吧
前后缀
前缀:子串的左端点为 1 的子串
后缀:子串的右端点是 n 的子串
最长公共前 / 后缀:s,t 的前缀 l 如果相同,且 l 是最长的那个,则 l 为最长公共前缀,用 \(\mathbf{lcp(s, t)}\) 表示, 后缀同理,用 \(\mathbf{lcs(s, t)}\) 表示
匹配
匹配:称 t 匹配 s 指当且仅当 t 在 s 中出现
模式串:用于匹配的
文本串:用于被匹配的
字典:一堆模式串
Border 理论
Border 理论的开山之作: kmp
\(KMP\),是由\(K-(Donald E. Knuth),M-(James H. Morris),P-(Vaughan R. Pratt)\)三位巨佬提出,解决\(\mathbf{Border}\)的高效算法之一
字符串算法都会考虑一个东西:\(\Sigma\)!也就是0xFF中的字符集。
比如后面的\(\mathbf{AC自动机}\)是\(\mathbf{O(n |\Sigma|)}\)的时间复杂度,如果字符集太大,就会失效
但\(\mathbf{KMP}\)并不是,他的时间是\(\mathbf{O(n)}\),与字符集不挂钩。连字符集为\([-10^9,10^9]\)的范围都可以。
那让我们看看那三位巨佬是怎么推导出\(\mathbf{KMP}\)的
我们在理解\(\mathbf{Border}\)理论的重要性以后,我们便可以先定义一个\(fail_i\)。
\(fail_i\):前缀为\([1,i]\)的\(Border\)
假设我们已经求出了\(fail_{i-1}\),那我们如何求出\(fail_i\)?
我们给出引理1
引理1:\(i\)的第二长公共前后缀是\(fail_{fail_i}\)
不证自明。
那我们持续套用这个公理,就可以做到把\([1,i-1]\)的所有公共前后缀给求出来,然后我们尝试往后加字符,便可以了。
时间应该是\(O(n^2)\),但却是\(O(n)\)。
原因是\(fail_i \le fail_{i-1}+1\)
所以每次跳的均摊便可做到\(O(n)\)
给出代码:
vector<ll> f(n+1, 0);
for(int i = 2; i <= n; i++){
f[i] = f[i-1];
while(f[i] && t[f[i]+1] != t[i])f[i] = f[f[i]];//!!!
if(t[f[i]+1] == t[i])f[i]++;
// if(f[i] || (t[1] == t[i])) f[i]++;普通
}
上面给出了两种代码,普通的是直接判断,而不加注释也很容易理解
为什么我们将代表\(Border\)的数组写成\(fail\)?
\(fail\)是失配的意思,原因就是我们在求解的过程中总是在匹配从而被叫做失配
现在我们看一下这道题:P3375 【模板】KMP - 洛谷
第二问就是将\(fail_i(t)\)输出出来
那第一问呢?
我们想要做匹配,当然匹配失败不能直接回退到初始状态,那我们可以用\(t\)的\(fail\)进行最小化回退
代码:
ll k = 0;
for(int i = 1; i <= n; i++){
while(k&&(k == m || t[k+1] != s[i])) k = f[k];
if(t[k+1] == s[i]) k++;
if(k == m)cout << i-m+1 <<endl;
}
现在我们考虑 kmp自动机。
如果我们并没有证明 kmp 均摊下来是 O(n) 怎么办?
考虑优化。
我们定义 \(\mathbf{trans}\)
那么 \(fail_i\) 就可以 \(O(1)\) 跳了。总体时间复杂度为 \(O(n|\sum|)\)
因为 kmp 自动机是一种 dp 思想,所以上面可以套上一个 dp
AC自动机
我们考虑一个文本,和词典匹配。
我们考虑在 Trie 上 kmp 。
但,如果在 Trie 上,则 kmp 的时间就是错误的。
考虑优化,自然而然想到 kmp 自动机。
那么我们对 Trie 上建自动机,\(O(1)\) 跳,然后用 BFS 建树,时间复杂度为 \(O(n|\sum|)\)
建 link 树,转化成到根链上所有是一个字符串结尾的个数。
link 树是一个非常好用的东西,虽然他和 kmp 的 fail 是一样的,但和 SAM 的 link 有异曲同工之妙
现在我们来看点题吧
杂题
CF1200E
对于每一个字符串,把以前的答案拼在后面跑 kmp ,看最长 border 。时间复杂度最坏到\(O(n^2)\)
发现拼在一起的最长只有 \(|s|\) ,时间复杂度为 \(\sum |S_i|\) 可以通过此题
Gym105423H
kmp 自动机上 dp。每次匹配进度满了就清空。时间 \(nk|s||\Sigma|\)。
CF163E
离线,对于每一个文本串建 AC 自动机。对于加入 / 删除一个串,等同于在 link 树中对于子树 +/-1 。
查询就是对文本串所有位置对应的节点,在 Fail 树上做单点加,然后查询该模式串结尾节点的子树和
用 fewicktree 维护。
浙公网安备 33010602011771号