字符串

字符串

字符串,从笔者入门时就以进入,但真正的字符串远没有那么简单,如果读者硬要抬杠,那么不好意思,笔者将拿出这道题:

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}\)

\[\text{trans}_{i,ch} = \begin{cases} i+1 & t_{i+1} = ch, \\ \text{trans}(\text{fail}_i,ch) & t_{i+1} \neq ch \end{cases} \]

那么 \(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 维护。

posted on 2026-08-26 10:51  秋天的宇宙  阅读(4)  评论(0)    收藏  举报

导航