字符串学习笔记 (1)
www.luogu.com.cn/training/151054
字符串学习笔记 (1) 的主要内容:KMP、exKMP、KMPAM。
练习题的排列顺序是按照我的做题顺序,与难度无关。
一、字符串基础知识
注:若无特殊说明则默认字符串的下标从 开始, 等字母代表的是字符串。
模式串 (, pattern) 与文本串 (, text):从文本串中进行模式匹配寻找模式串。
子串:从原串中选取连续的一段字符串,空串也是子串。
前缀: 表示 前 个字符构成的子串。
后缀: 表示 后 个字符构成的子串。
最长公共前缀: 表示 和 一样的最长的前缀。
最长公共后缀: 表示 和 一样的最长的后缀。
注意任何子串都既是某个前缀的后缀也是某个后缀的前缀,子串、前缀、后缀都可以为 可以为空串。
period:若 ,则称 是 的一个 period(周期)。
border:若 ,则称 是 的一个 border(相同的前后缀)。
最长 border: 表示 的最长的 border,若 没有 border 则 。
最长公共 border: 表示 和 一样的最长的 border,若不存在则 。
注意 period 不能为 不能为 ,border 不能为 不能为空串。
一些基础的 border 的特殊性质:
- 若 是 的 border,则 是 的 period。
- 若 是 的 period,则 是 的 border。
- 若 是 的 border,,则 是由 个 拼成的。
- 若 是由 个 拼成的,则 是 的 border,。
- 若 , 是 的 border,则 是 的 border。
- 若 , 是 的 border,,则 是 的 border。
- 若 是 的 border, 是 的 border,则 是 的 border。
- 若 是 的 border, 是 的 border,,则 是 的 border。
- 构成了 的所有 border,即 的所有 border 环环相扣,由长到短被一条链串在一起。
- 若存在长度大于 的 border,则必然存在长度小于 的 border,但逆命题不一定成立。
其中第 1~2 条是 border 与 period 的关系,第 1~2, 3~4, 5~6 条为互逆命题,第 7~9 条是 border 的传递性。
这些特殊性质都非常简单,读者自证不难,举个栗子画个图就很容易证明。
二、字符串基础算法
1、Hash 函数
把 看成 进制数,一般令 。
一般对 取模,即利用 自然溢出代替常数巨大的取模运算。
显然有:。
2、Hash+二分
对于有单调性的信息纯 Hash+二分时间复杂度一般会比正解多 ,能拿到较高分。
例如:Hash+二分代替 Manacher 求最长回文子串 ,求 SA 。
但是大多数题的纯 Hash 或者纯 Hash+二分的做法都能用其他字符串科技以思维难度更低,时间复杂度更优或常数更小来取代。
所以 Hash 是一个辅助工具、锦上添花的东西,不能只靠 Hash 做字符串题。
3、字符串的最小表示法
给定 ,若不断把它的首字符放到末尾,最终会得到最多 个不同的字符串,称这 个字符串是循环同构的。
这些字符串中字典序最小的一个称为 的最小表示。如何求最小表示?一种方法是最小表示法:
先将 复制一倍接到末尾上,那么 即为以 开头的 的循环同构串,记为 。
假设比较 和 ,第一个不相同的位置在 处,即 。
若 ,则 一定不是 的最小表示,因为 的 处更小。
同理若 ,则 一定不是 的最小表示。
因此可以通过两个指针 不断向后移动比较循环同构串的大小,当其中一个移动到结尾时就找到了最小表示。
若每次比较向后扫描 的长度,则 其中一个指针必然向后移动 。
又因为 两个指针总共向后移动小于 ,因此时间复杂度为 。
代码是 P1368 【模板】最小表示法 的,另外一题代码几乎完全一样。

1 const int N = 3e5 + 10; 2 int n, a[N << 1]; 3 4 int main() 5 { 6 read(n); 7 for (int i = 1; i <= n; ++i) read(a[i]); 8 for (int i = 1; i <= n; ++i) a[n + i] = a[i]; 9 int i = 1, j = 2, k; 10 while (i <= n && j <= n) 11 { 12 for (k = 0; k < n && a[i + k] == a[j + k]; ++k); 13 if (k == n) break; 14 a[i + k] > a[j + k] ? i += k + 1 : j += k + 1; 15 if (i == j) ++i; 16 } 17 int ans = Min(i, j); 18 for (int i = ans; i < ans + n; ++i) cout << a[i] << ' '; 19 return 0; 20 }
三、KMP
1、KMP 算法
KMP 算法,英文全称 the Knuth-Morris-Pratt algorithm。
设两个指针 和 表示当前文本串 匹配到 ,模式串 匹配到 。
KMP 的思想为:找一个最大的 满足 ,当 时意味着匹配到了模式串 。
考虑 由 向右移动一位,若 ,则显然 即可。
但是若 ,则新的 与原先的 一定满足 且 是 的 border。
这是因为 显然是 的前缀和 的后缀,又因为 所以这个 的前缀也是 的前缀。
因此相当于找一个 的最长的 border 满足 。
由 border 的特殊性质 9,由长到短找到第一个满足 的 border 即为最长的 border。
因为找的都是模式串 的前缀的 border,与文本串 无关,所以可以预处理 表示 的最长的 border 的长度。
因此 KMP 的 next 函数的实质是:,即 的前缀的最长 border 的长度。
如何求 next 函数?设两个指针 和 表示当前已求出 ,且 ,现在需要求 。
由 border 的特殊性质 6,若 ,则 。
由 border 的特殊性质 9, 的所有 border 是 。
所以从 开始看 是否成立,不成立就 用 next 函数往回退,找出最长 border。
虽然 KMP 的预处理 next 函数过程和模式匹配过程都有进()有退(),但是由于每次进都是 ,进的次数最多为 , 非负,所以 ,因此均摊后总时间复杂度是线性的,为 。
根据整个 KMP 算法可以显然发现 next 函数满足一个非常非常非常重要的不等式: 恒成立。
注意类似 next 函数或者能够抽象成类似 next 函数的函数 (定义在字符串上或者能够抽象成在字符串上且从 开始不断 则 严格单调递减的函数)若也满足不等式 恒成立,则 的计算也可以用 KMP 均摊成线性复杂度。
代码是 P3375 【模板】KMP字符串匹配 的,另外一题代码几乎完全一样。

1 const int N = 1e6 + 10; 2 char s1[N], s2[N]; 3 int n, m, nxt[N]; 4 5 void build(const char *s) 6 { 7 nxt[1] = 0; 8 for (int j = 0, i = 2; i <= n; ++i) 9 { 10 while (j && s[i] != s[j + 1]) j = nxt[j]; 11 if (s[i] == s[j + 1]) ++j; 12 nxt[i] = j; 13 } 14 } 15 16 void match(const char *s, const char *t) 17 { 18 for (int j = 0, i = 1; i <= m; ++i) 19 { 20 while (j && t[i] != s[j + 1]) j = nxt[j]; 21 if (t[i] == s[j + 1]) ++j; 22 if (j == n) cout << i - n + 1 << '\n'; 23 } 24 } 25 26 int main() 27 { 28 scanf("%s%s", s1 + 1, s2 + 1); 29 n = strlen(s2 + 1), m = strlen(s1 + 1); 30 build(s2); match(s2, s1); 31 for (int i = 1; i <= n; ++i) cout << nxt[i] << ' '; 32 return 0; 33 }
2、KMP 与失配树
失配树,又称 next 树,fail 树,是只挂了一个串的 ACAM 的 fail 树。
的失配树是将 视为 的父节点,由 共 个点构成的一棵树, 为根节点,深度为 。
由于 只有一个 且 ,所以显然是一棵树。
失配树的特殊性质:
- 点 的所有祖先都是 的 border,点 的父亲是 。
- 没有祖先关系的两个点 没有 border 关系。
- 祖先的节点编号一定小于后代的节点编号。
性质非常显然易证。
KMP 预处理 可看成:从 往上走,找到第一个满足 的点 ,并 。
KMP 模式匹配过程可看成:从 往上走,找到第一个满足 的点 ,并 。
失配树是 KMP 的核心,KMP 看似难以理解,考虑成失配树上的操作会很容易明白,很多 KMP 难题考虑失配树直接变得很简单。
题意:给定 , 次询问,每次给 求 。。
由失配树的特殊性质 1,答案显然是失配树上 和 的 lca,当然若 是祖先后代关系应该是 lca 的父亲。

1 const int N = 1e6 + 10; 2 const int logN = 20; 3 char s[N]; 4 int nxt[N], dep[N], fa[logN][N]; 5 6 inline int lca(int u, int v) 7 { 8 if (dep[u] < dep[v]) swap(u, v); 9 for (int i = logN - 1; ~i; --i) 10 if (dep[fa[i][u]] >= dep[v]) u = fa[i][u]; 11 if (u == v) return fa[0][u]; 12 for (int i = logN - 1; ~i; --i) 13 if (fa[i][u] != fa[i][v]) u = fa[i][u], v = fa[i][v]; 14 return fa[0][u]; 15 } 16 17 int main() 18 { 19 scanf("%s", s + 1); int n = strlen(s + 1); 20 nxt[1] = 0; dep[1] = 1; fa[0][1] = 0; 21 for (int j = 0, i = 2; i <= n; ++i) 22 { 23 while (j && s[i] != s[j + 1]) j = nxt[j]; 24 if (s[i] == s[j + 1]) ++j; 25 nxt[i] = j; dep[i] = dep[j] + 1; fa[0][i] = j; 26 } 27 for (int j = 1; j < logN; ++j) 28 for (int i = 1; i <= n; ++i) 29 fa[j][i] = fa[j - 1][fa[j - 1][i]]; 30 int q, u, v; read(q); 31 while (q--) read(u, v), cout << lca(u, v) << '\n'; 32 return 0; 33 }
3、KMP 练习
- KMP 练习 1
题意 1:给定 ,求最短的 ,使得 是若干个 拼成的字符串的前缀。。
题意 2:给定 ,求字典序最小的 ,使得 是若干个 拼成的字符串的前缀。。
题意 3:给定 ,求最短的 ,使得 恰好是若干个 拼成的字符串。。
题意 4:给定 ,求字典序最小的 ,使得 恰好是若干个 拼成的字符串。。
题意 5:给定 ,求最短的 ,使得 是若干个 拼成的字符串的子串,若有多解输出任意一个。。
题意 6:给定 ,求最短的 ,使得 是若干个 拼成的字符串的子串,若有多解输出字典序最小的解。。
不难发现这 6 个题意实际都是与 的最短周期有关,答案都与 有关。
注意题意 3 和 4 还要判断 ,题意 6 还要在题意 5 的基础上求答案的最小表示。
对于题意 3 和 4 可以发现一个结论:合法答案的长度必然是最短答案的长度的倍数,利用 border 的性质易证。

1 const int N = 2e3 + 10; 2 char s[N]; 3 int nxt[N]; 4 5 int main() 6 { 7 int T; read(T); 8 while (T--) 9 { 10 scanf("%s", s + 1); int n = strlen(s + 1); 11 nxt[1] = 0; 12 for (int j = 0, i = 2; i <= n; ++i) 13 { 14 while (j && s[i] != s[j + 1]) j = nxt[j]; 15 if (s[i] == s[j + 1]) ++j; 16 nxt[i] = j; 17 } 18 int l = n - (n - nxt[n] << 1) + 1, Case = 8, i = l; 19 while (Case--) 20 { 21 putchar(s[i]); ++i; 22 if (i > n) i = l; 23 } 24 puts("..."); 25 } 26 return 0; 27 }
代码是 AcWing 141. 周期 的,另外两题代码几乎完全一样。

1 const int N = 1e6 + 10; 2 char s[N]; 3 int nxt[N]; 4 5 int main() 6 { 7 int T = 0; 8 while (true) 9 { 10 int n; read(n); if (!n) break; 11 scanf("%s", s + 1); nxt[1] = 0; 12 for (int j = 0, i = 2; i <= n; ++i) 13 { 14 while (j && s[i] != s[j + 1]) j = nxt[j]; 15 if (s[i] == s[j + 1]) ++j; 16 nxt[i] = j; 17 } 18 cout << "Test case #" << ++T << '\n'; 19 for (int i = 1; i <= n; ++i) 20 if (!(i % (i - nxt[i])) && i / (i - nxt[i]) > 1) 21 cout << i << ' ' << i / (i - nxt[i]) << '\n'; 22 cout << '\n'; 23 } 24 return 0; 25 }

1 const int N = 1e6 + 10; 2 char s[N]; 3 int nxt[N]; 4 5 int main() 6 { 7 int n; read(n); scanf("%s", s + 1); 8 for (int j = 0, i = 2; i <= n; ++i) 9 { 10 while (j && s[i] != s[j + 1]) j = nxt[j]; 11 if (s[i] == s[j + 1]) ++j; 12 nxt[i] = j; 13 } 14 cout << n - nxt[n]; 15 return 0; 16 }
- KMP 练习 2
题意:给定 ,求字典序最小的 ,使得 是若干个 拼成的字符串的子串。。
虽然写的是“KMP 练习 2”但是不知道是否能用 KMP 或者其他什么神仙做法做到线性复杂度……
给一个可能 hack 掉很多做法的数据: 为 ,答案是 ,并不是 。
暴力是先求出每个串的前缀的最小表示(参考 P5334 [JSOI2019]节日庆典)再找合法的取字典序最小的,用 SA 可以做到一个 。
- KMP 练习 3
给定文本串 和模式串 ,求最多能模式匹配互不重叠的模式串 的个数。。
显然从左到右贪心能选就选。
- KMP 练习 4
题意:给定 ,对每个 都求最长的 ,使得 且 是若干个 拼成的字符串的前缀。。
实际就是求 的每个前缀的最长周期,答案显然为 的最短 border,在失配树上就是 的所有祖先中离 最近的那个点。
可以先构造出失配树再 DFS 一遍,也可以将 进行路径压缩优化,时间复杂度为 。

1 const int N = 1e6 + 10; 2 char s[N]; 3 int nxt[N], fa[N]; 4 5 inline int find(int x) { return nxt[x] ? fa[x] = find(fa[x]) : x; } 6 7 int main() 8 { 9 int n; read(n); scanf("%s", s + 1); ll ans = 0; 10 nxt[1] = 0; fa[1] = 1; 11 for (int j = 0, i = 2; i <= n; ++i) 12 { 13 while (j && s[i] != s[j + 1]) j = nxt[j]; 14 if (s[i] == s[j + 1]) ++j; 15 nxt[i] = j; !j ? fa[i] = i : fa[i] = find(j); 16 } 17 for (int i = 1; i <= n; ++i) 18 if (nxt[i]) ans += i - fa[i]; 19 cout << ans; 20 return 0; 21 }
- KMP 练习 5
题意:给定 的字符矩阵,求最小连续子矩阵,使得该子矩阵无限复制扩张后的矩阵包含原矩阵。。
实际上就是 KMP 练习 1 的题意 1 的字符串变成了二维的情况,将每一行和每一列都当成一个整体(最好 Hash 成一个数)即可。
对 行整体求出最短周期 ,对 列整体求出最短周期 ,则答案显然为 的字符矩阵。
KMP 时间复杂度为 ,用 Hash 优化比较运算能做到 ,但输入量为 所以总时间复杂度还是 。

1 const int N = 1e4 + 10, M = 80; 2 const ull p = 131; 3 char s[N][M]; 4 ull h[N]; 5 int nxt[N]; 6 7 int main() 8 { 9 int n, m; read(n, m); 10 for (int i = 1; i <= n; ++i) 11 scanf("%s", s[i] + 1); 12 13 for (int i = 1; i <= n; ++i) 14 for (int j = 1; j <= m; ++j) 15 h[i] = h[i] * p + s[i][j] - 96; 16 nxt[1] = 0; 17 for (int j = 0, i = 2; i <= n; ++i) 18 { 19 while (j && h[i] != h[j + 1]) j = nxt[j]; 20 if (h[i] == h[j + 1]) ++j; 21 nxt[i] = j; 22 } 23 int r = n - nxt[n]; 24 25 memset(h, 0, sizeof(h)); 26 for (int j = 1; j <= m; ++j) 27 for (int i = 1; i <= n; ++i) 28 h[j] = h[j] * p + s[i][j] - 96; 29 nxt[1] = 0; 30 for (int j = 0, i = 2; i <= m; ++i) 31 { 32 while (j && h[i] != h[j + 1]) j = nxt[j]; 33 if (h[i] == h[j + 1]) ++j; 34 nxt[i] = j; 35 } 36 int c = m - nxt[m]; 37 38 cout << r * c; 39 return 0; 40 }
- KMP 练习 6
题意:给定 ,对每个 求长度不超过 的 border 数量。。
实际上就是求失配树上 的祖先中第一个编号不超过 的点的深度,直接暴力倍增就可以做到 。
令 表示 的长度不超过 的最长 border 的长度,则不等式 恒成立。
证明:
因为 ,所以 。
又因为 是 某个 border 的长度, 是 长度不超过 的最长 border 的长度,所以 即 。
证毕。
因此用 KMP 均摊线性求出 即可,注意每次进后若超过 要回退一次,时间复杂度为 。

1 const int N = 1e6 + 10; 2 const ll p = 1e9 + 7; 3 char s[N]; 4 int nxt[N], dep[N]; 5 6 int main() 7 { 8 int T; read(T); 9 while (T--) 10 { 11 scanf("%s", s + 1); 12 int n = strlen(s + 1); ll ans = 1; 13 memset(nxt, 0, sizeof(nxt)); 14 memset(dep, 0, sizeof(dep)); 15 nxt[1] = 0; dep[1] = 1; 16 for (int j = 0, i = 2; i <= n; ++i) 17 { 18 while (j && s[i] != s[j + 1]) j = nxt[j]; 19 if (s[i] == s[j + 1]) ++j; 20 nxt[i] = j; dep[i] = dep[j] + 1; 21 } 22 for (int j = 0, i = 1; i <= n; ++i) 23 { 24 while (j && s[i] != s[j + 1]) j = nxt[j]; 25 if (s[i] == s[j + 1]) ++j; 26 if (j << 1 > i) j = nxt[j]; 27 ans = (ans * (dep[j] + 1)) % p; 28 } 29 cout << ans << '\n'; 30 } 31 return 0; 32 }
- KMP 练习 7
题意:给定文本串 ,求最短模式串 ,使得在 中出现过的所有的 能将 的每个位置都至少覆盖一次。。
的首尾字符都要被 覆盖到所以显然 是 的 border,所以答案只能是失配树上 这条链上第一个合法的节点。
由失配树的特殊性质 1, 是 的所有后代的 border,所以 的子树内所有节点排序后相邻两个节点的差的最大值(即最大间隔 )就是以 作为模式串 , 中相邻两个模式串 的距离的最大值,只要 显然是合法的。
随着点的不断删除 显然单调不减,因此只需要一种数据结构支持删除 ,查询 的前驱和后继,可以考虑双向链表。
因此维护当前 的子树内的点即可,有意思的是由失配树的特殊性质 3, 的祖先即使不删除显然也不会影响答案(懒)。
由于每个点最多只会被删除一次,所以时间复杂度为 。
当然这道题还有一种不用失配树的更简单的做法:直接考虑 DP,设 表示 的答案,实质是合法最短 border 长度。
显然 只可能是 或 ,它能由 转移当且仅当存在一个 满足 。
因此开一个桶 来存储 的值,表示以 作为模式串 ,文本串为 合法的最大的 。
时间复杂度也是 。
第一份代码是失配树的,第二份代码是 DP 的。

1 const int N = 5e5 + 10; 2 char s[N]; 3 int maxgap, nxt[N], pre[N], suf[N]; 4 bool border[N]; 5 vector<int> son[N]; 6 7 void del(int u) 8 { 9 if (~pre[u] && ~suf[u]) maxgap = Max(maxgap, suf[u] - pre[u]); 10 suf[pre[u]] = suf[u]; pre[suf[u]] = pre[u]; pre[u] = suf[u] = -1; 11 for (int i = 0; i < son[u].size(); ++i) del(son[u][i]); 12 } 13 14 int main() 15 { 16 scanf("%s", s + 1); int n = strlen(s + 1); 17 nxt[1] = 0; son[0].emplace_back(1); 18 for (int j = 0, i = 2; i <= n; ++i) 19 { 20 while (j && s[i] != s[j + 1]) j = nxt[j]; 21 if (s[i] == s[j + 1]) ++j; 22 nxt[i] = j; son[j].emplace_back(i); 23 } 24 for (int j = n; j; j = nxt[j]) border[j] = true; 25 for (int i = 1; i <= n; ++i) pre[i] = i - 1, suf[i] = i + 1; 26 pre[0] = suf[0] = pre[n + 1] = suf[n + 1] = -1; maxgap = 1; 27 int u = 0, v; 28 while (u != n) 29 { 30 for (int i = 0; i < son[u].size(); ++i) 31 border[son[u][i]] ? (void)(v = son[u][i]) : del(son[u][i]); 32 if (maxgap <= v) { cout << v; return 0; } 33 u = v; 34 } 35 cout << n; 36 return 0; 37 }

1 const int N = 5e5 + 10; 2 char s[N]; 3 int nxt[N], dp[N], b[N]; 4 5 int main() 6 { 7 scanf("%s", s + 1); int n = strlen(s + 1); 8 for (int j = 0, i = 2; i <= n; ++i) 9 { 10 while (j && s[i] != s[j + 1]) j = nxt[j]; 11 if (s[i] == s[j + 1]) ++j; 12 nxt[i] = j; 13 } 14 for (int i = 1; i <= n; ++i) 15 dp[i] = i - nxt[i] <= b[dp[nxt[i]]] ? dp[nxt[i]] : i, b[dp[i]] = i; 16 cout << dp[n]; 17 return 0; 18 }
- KMP 练习 8
题意:给定字符集 和长为 仅由 中的字符构成的 ,求长为 仅由 中的字符构成的 的个数,使得 是 的子串,答案对 取模。。
设 表示当前 构造到 , 匹配到 的答案,枚举 处的字母,此时 匹配到 ,则 。
注意 不转移,相当于 第一次出现时统计答案,显然不会算重,最终答案为 ,这样能做到 。
只能转移到 , 只与 有关与 无关, 很小但 巨大,这都启发我们矩阵快速幂。
将 看成 的行向量,将转移看成 的列向量,根据 预处理出转移矩阵即可。
最终答案的计算可以再加一列记录 的前缀和,也可以容斥计算, 显然是 不是 的子串的答案。
预处理 ,矩阵快速幂 ,所以总时间复杂度为 。

1 const int N = 25; 2 int n, m, p, ans; 3 char s[N], t[N]; 4 int nxt[N]; 5 6 struct matrix 7 { 8 int a[N][N]; 9 matrix() { clear(); } 10 inline void clear() 11 { 12 for (int i = 0; i <= m; ++i) 13 for (int j = 0; j <= m; ++j) 14 a[i][j] = 0; 15 } 16 inline void init() 17 { 18 for (int i = 0; i <= m; ++i) 19 for (int j = 0; j <= m; ++j) 20 a[i][j] = i == j; 21 } 22 } f, g; 23 inline matrix operator * (const matrix &x, const matrix &y) 24 { 25 matrix ans; 26 for (int i = 0; i <= m; ++i) 27 for (int j = 0; j <= m; ++j) 28 for (int k = 0; k <= m; ++k) 29 addmod(ans.a[i][j] += x.a[i][k] * y.a[k][j] % p, p); 30 return ans; 31 } 32 inline matrix quickpow(matrix x, int y) 33 { 34 matrix ans; ans.init(); 35 for (; y; x = x * x, y >>= 1) 36 if (y & 1) ans = ans * x; 37 return ans; 38 } 39 40 int main() 41 { 42 read(n, m, p); scanf("%s", s + 1); 43 for (int j = 0, i = 2; i <= m; ++i) 44 { 45 while (j && s[i] != s[j + 1]) j = nxt[j]; 46 if (s[i] == s[j + 1]) ++j; 47 nxt[i] = j; 48 } 49 f.a[0][0] = 1; 50 for (int i = 0; i < m; ++i) 51 for (int c = 0; c < 10; ++c) 52 { 53 int j = i; 54 while (j && c + '0' != s[j + 1]) j = nxt[j]; 55 if (c + '0' == s[j + 1]) ++j; 56 ++g.a[i][j]; 57 } 58 f = f * quickpow(g, n); 59 for (int i = 0; i < m; ++i) addmod(ans += f.a[0][i], p); 60 cout << ans; 61 return 0; 62 }
- KMP 练习 9
题意:给定 ,对每个 都求出 是满足 的最大值的 的个数。。
问题等价于将 的所有后缀与 匹配,后缀的首字符和 的首字符对齐,对每个 求匹配长度恰好为 的后缀个数。
看起来似乎很难用 KMP 解决,但可以用差分思想去转化,若问题是匹配长度至少为 呢?
若 匹配长度为 ,则以 为首字符的后缀的匹配长度分别至少为 。
因此相当于失配树上将 这条链上的点的答案都 ,先打标记最后遍历失配树求答案,时间复杂度为 。

- KMP 练习 10
题意:给定文本串 和模式串 ,将 中的 不断删除,求最后的 ,注意删除一个 后两边会拼在一起。。
用一个栈来存储当前的 ,再记录当前的 的前缀的匹配长度 即可。

- KMP 练习 11
题意:给定 ,可以将 中连续几个相同的部分压缩成一个,操作可以进行若干次,求最终 的长度最小值。。
考虑区间 DP,实际上压缩就是 KMP 练习 1 的题意 3 的问题,每个区间(子串)都找最短循环节即可,时间复杂度为 。
注意 P4302 [SCOI2003]字符串折叠、UVA1630 串折叠 Folding 压缩后不一定更优。
代码是 UVA1630 串折叠 Folding 的,另外两题代码几乎完全一样。

1 const int N = 105; 2 char str[N]; 3 int dp[N][N]; 4 5 inline int digit(int x) 6 { 7 int t = 0; 8 if (!x) ++t; 9 while (x) x /= 10, ++t; 10 return t; 11 } 12 13 inline bool check(int l, int r, int &p) 14 { 15 int nxt[N]; char s[N]; int n = r - l + 1; 16 memset(nxt, 0, sizeof(nxt)); nxt[1] = 0; 17 for (int i = l; i <= r; ++i) s[i - l + 1] = str[i]; 18 for (int j = 0, i = 2; i <= n; ++i) 19 { 20 while (j && s[i] != s[j + 1]) j = nxt[j]; 21 if (s[i] == s[j + 1]) ++j; 22 nxt[i] = j; 23 } 24 p = n - nxt[n]; 25 if (n % p || n / p <= 1) return false; 26 if (p == 1 && n / p <= 3) return false; 27 if (p == 2 && n / p <= 2) return false; 28 return true; 29 } 30 31 inline void print(int l, int r) 32 { 33 if (l == r) { putchar(str[l]); return; } 34 int p, n = r - l + 1; 35 if (check(l, r, p)) 36 { cout << n / p; putchar('('); print(l, l + p - 1); putchar(')'); return; } 37 for (int k = l; k < r; ++k) 38 if (dp[l][r] == dp[l][k] + dp[k + 1][r]) 39 { print(l, k), print(k + 1, r); return; } 40 } 41 42 int main() 43 { 44 while (~scanf("%s", str + 1)) 45 { 46 int n = strlen(str + 1); memset(dp, 0x3f, sizeof(dp)); 47 for (int i = 1; i <= n; ++i) dp[i][i] = 1; 48 for (int len = 2; len <= n; ++len) 49 for (int p, l = 1, r = l + len - 1; r <= n; ++l, ++r) 50 { 51 dp[l][r] = check(l, r, p) ? digit(len / p) + 2 + dp[l][l + p - 1] : len; 52 for (int k = l; k < r; ++k) 53 dp[l][r] = Min(dp[l][r], dp[l][k] + dp[k + 1][r]); 54 } 55 print(1, n); cout << '\n'; 56 } 57 return 0; 58 }
- KMP 练习 12
题意:给定文本串 和模式串 ,判断 是否在 中出现恰好一次。。
直接匹配求次数即可。
- KMP 练习 13
题意:给定 ,求它的最长前缀 (不能为空但可以为 )使得 翻转后恰好也是 的后缀。。
将 翻转并和原来的 求 LCP 即可。
- KMP 练习 14
题意:给定 的文本字符矩阵和 的模式字符矩阵,求出现次数。。
与 KMP 练习 5 的做法类似,二维 Hash 后直接匹配即可,时间复杂度为 。
- KMP 练习 15
题意:给定仅由小写字母和问号构成的文本串 和仅由小写字母构成的模式串 ,问号可以替换为任意一个小写字母,求 在 中的出现次数的最大值。。
与 KMP 练习 8 的做法类似,由于 与 同阶,即使预处理转移矩阵时间复杂度仍然不会变。
因此直接暴力 DP 求解即可,时间复杂度为 。
- KMP 练习 16
题意:给定两段墙 和 ,求 墙中与 墙顶部形状相同的区间数。。
形状相同即差分后子串相同,然后求子串出现次数即可,注意 的情况需要特判。
- KMP 练习 17
题意:给定 ,求最长的 ,满足 既是 的前缀也是 的后缀还是 的子串(不含前后缀)。。
首先答案显然一定是 的某个 border,其次子串必然是某个后缀的前缀。
- KMP 练习 18
题意:
- KMP 练习 19
题意:
- KMP 练习 20
题意:
-
- 例题:
四、exKMP
1、exKMP 算法
exKMP,又称扩展 KMP,Z 函数,Z Algorithm。
实际上 KMP 与 exKMP 的思想类似,过程相像,学懂了 KMP 后 exKMP 很容易理解。
设 ,定义函数 ,即 与以 开头的后缀的最长公共前缀的长度。
特别地, 的取值为 还是 需要看题意,这里的前缀是否必须是真前缀(不包含 本身)。
被称为 的 Z 函数, 被称为 的匹配段,又称 的 Z-box。
计算 的过程与 类似,也需要利用 ,从 到 顺次计算。
我们需要维护右端点最靠右的 Z-box ,显然它也是 ,计算 时保证 ,令 的初值为 。
若 ,显然 ,因此若 ,则 ,否则 后暴力向右扩展。若 ,直接暴力向右扩展。最后注意用求出的 更新 即可。
这个网站 是对 exKMP 预处理 函数的演示,模式匹配的过程也跟预处理的过程非常像,画个图不难自推。
考虑 只会向前进不会向后退,,所以时间复杂度均摊线性为 。
不难发现实质上 KMP 是 与 的前缀的最长后缀匹配,exKMP 是 与 的后缀的最长前缀匹配。
它们都能处理子串模式匹配问题,KMP 能做的题 exKMP 都能做,exKMP 能做的题 KMP 都能做。
只不过有的时候某个信息更方便处理或处理的时间复杂度更优,我们需要根据实际情况选择用 KMP 还是 exKMP。

1 const int N = 2e7 + 10; 2 char s[N], t[N]; 3 int z[N], Z[N]; 4 5 int main() 6 { 7 scanf("%s%s", t + 1, s + 1); 8 int n = strlen(s + 1), m = strlen(t + 1); z[1] = n; 9 for (int l = 0, r = 0, i = 2; i <= n; ++i) 10 { 11 if (i <= r) z[i] = Min(z[i - l + 1], r - i + 1); 12 while (i + z[i] <= n && s[i + z[i]] == s[z[i] + 1]) ++z[i]; 13 if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1; 14 } 15 for (int l = 0, r = 0, i = 1; i <= m; ++i) 16 { 17 if (i <= r) Z[i] = Min(z[i - l + 1], r - i + 1); 18 while (i + Z[i] <= m && t[i + Z[i]] == s[Z[i] + 1]) ++Z[i]; 19 if (i + Z[i] - 1 > r) l = i, r = i + Z[i] - 1; 20 } 21 ll sum = 0; for (int i = 1; i <= n; ++i) sum ^= i * (z[i] + 1ll); cout << sum << '\n'; 22 sum = 0; for (int i = 1; i <= m; ++i) sum ^= i * (Z[i] + 1ll); cout << sum; 23 return 0; 24 }
2、KMP 与 exKMP 的相互转化
初学时可能会有疑问,为什么“KMP 能做的题 exKMP 都能做,exKMP 能做的题 KMP 都能做”?
因为在不知道原串的前提下,只知道 可以求出 ,只知道 也可以求出 ,即它们之间可以相互转化。
Dan Gusfield 对其进行了详细证明:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!