字符串学习笔记 (1)
www.luogu.com.cn/training/151054
字符串学习笔记 (1) 的主要内容:KMP、exKMP、KMPAM。
练习题的排列顺序是按照我的做题顺序,与难度无关。
一、字符串基础知识
注:若无特殊说明则默认字符串的下标从 $1$ 开始,$s,t$ 等字母代表的是字符串。
模式串 ($P$, pattern) 与文本串 ($T$, text):从文本串中进行模式匹配寻找模式串。
子串:从原串中选取连续的一段字符串,空串也是子串。
前缀:$pre(s,k)$ 表示 $s$ 前 $k\ (0\le k\le\vert s\vert)$ 个字符构成的子串。
后缀:$suf(s,k)$ 表示 $s$ 后 $k\ (0\le k\le\vert s\vert)$ 个字符构成的子串。
最长公共前缀:$lcp(s,t)$ 表示 $s$ 和 $t$ 一样的最长的前缀。
最长公共后缀:$lcs(s,t)$ 表示 $s$ 和 $t$ 一样的最长的后缀。
注意任何子串都既是某个前缀的后缀也是某个后缀的前缀,子串、前缀、后缀都可以为 $s$ 可以为空串。
period:若 $0<k<\vert s\vert,\ \forall\ i\le \vert s\vert-k,\ s[i]=s[i+k]$,则称 $k$ 是 $s$ 的一个 period(周期)。
border:若 $0<k<\vert s\vert,\ pre(s,k)=suf(s,k)$,则称 $pre(s,k)$ 是 $s$ 的一个 border(相同的前后缀)。
最长 border:$lb(s)$ 表示 $s$ 的最长的 border,若 $s$ 没有 border 则 $\vert lb(s)\vert=0$。
最长公共 border:$lcb(s,t)$ 表示 $s$ 和 $t$ 一样的最长的 border,若不存在则 $\vert lcb(s,t)\vert=0$。
注意 period 不能为 $\vert s\vert$ 不能为 $0$,border 不能为 $s$ 不能为空串。
一些基础的 border 的特殊性质:
- 若 $pre(s,k)$ 是 $s$ 的 border,则 $\vert s\vert-k$ 是 $s$ 的 period。
- 若 $k$ 是 $s$ 的 period,则 $pre(s,\vert s\vert-k)$ 是 $s$ 的 border。
- 若 $pre(s,k)$ 是 $s$ 的 border,$\vert s\vert\bmod k=0$,则 $s$ 是由 $\dfrac{\vert s\vert}{k}$ 个 $pre(s,k)$ 拼成的。
- 若 $s$ 是由 $\dfrac{k}{s}$ 个 $pre(s,k)$ 拼成的,则 $pre(s,k)$ 是 $s$ 的 border,$\vert s\vert\bmod k=0$。
- 若 $k<j<i$,$pre(s,j)$ 是 $pre(s,i)$ 的 border,则 $pre(s,j-k)$ 是 $pre(s,i-k)$ 的 border。
- 若 $k<j<i$,$pre(s,j-k)$ 是 $pre(s,i-k)$ 的 border,$\forall\ 0\le x<k,\ s[j-x]=s[i-x]$,则 $pre(s,j)$ 是 $pre(s,i)$ 的 border。
- 若 $s$ 是 $t$ 的 border,$t$ 是 $r$ 的 border,则 $s$ 是 $r$ 的 border。
- 若 $s$ 是 $r$ 的 border,$t$ 是 $r$ 的 border,$\vert s\vert<\vert t\vert$,则 $s$ 是 $t$ 的 border。
- $lb(s),lb(lb(s)),lb(lb(lb(s))),\cdots$ 构成了 $s$ 的所有 border,即 $s$ 的所有 border 环环相扣,由长到短被一条链串在一起。
- $s$ 若存在长度大于 $\dfrac{\vert s\vert}{2}$ 的 border,则必然存在长度小于 $\dfrac{\vert s\vert}{2}$ 的 border,但逆命题不一定成立。
其中第 1~2 条是 border 与 period 的关系,第 1~2, 3~4, 5~6 条为互逆命题,第 7~9 条是 border 的传递性。
这些特殊性质都非常简单,读者自证不难,举个栗子画个图就很容易证明。
二、字符串基础算法
1、Hash 函数
把 $s$ 看成 $p$ 进制数,一般令 $a=1,\ b=2,\ \cdots,\ z=26,\ p=131\lor13331$。
一般对 $2^{64}$ 取模,即利用 $\verb@unsigned long long@$ 自然溢出代替常数巨大的取模运算。
显然有:$h(s+t)=h(s)\times p^{\vert t\vert}+h(t),\ h(t)=h(s+t)-h(s)\times p^{\vert t\vert}$。
2、Hash+二分
对于有单调性的信息纯 Hash+二分时间复杂度一般会比正解多 $\log$,能拿到较高分。
例如:Hash+二分代替 Manacher 求最长回文子串 $O(n\log{n})$,求 SA $O(n\log^2{n})$。
但是大多数题的纯 Hash 或者纯 Hash+二分的做法都能用其他字符串科技以思维难度更低,时间复杂度更优或常数更小来取代。
所以 Hash 是一个辅助工具、锦上添花的东西,不能只靠 Hash 做字符串题。
3、字符串的最小表示法
给定 $s$,若不断把它的首字符放到末尾,最终会得到最多 $n$ 个不同的字符串,称这 $n$ 个字符串是循环同构的。
这些字符串中字典序最小的一个称为 $s$ 的最小表示。如何求最小表示?一种方法是最小表示法:
先将 $s$ 复制一倍接到末尾上,那么 $s[i\cdots i+\vert s\vert-1]$ 即为以 $i$ 开头的 $s$ 的循环同构串,记为 $t[i]$。
假设比较 $t[i]$ 和 $t[j]$,第一个不相同的位置在 $k$ 处,即 $s[i+k]\not=s[j+k]$。
若 $s[i+k]>s[j+k]$,则 $t[i+x]\ (0\le x\le k)$ 一定不是 $s$ 的最小表示,因为 $t[j+x]$ 的 $x$ 处更小。
同理若 $s[i+k]<s[j+k]$,则 $t[j+x]\ (0\le x\le k)$ 一定不是 $s$ 的最小表示。
因此可以通过两个指针 $i,j$ 不断向后移动比较循环同构串的大小,当其中一个移动到结尾时就找到了最小表示。
若每次比较向后扫描 $k$ 的长度,则 $i,j$ 其中一个指针必然向后移动 $k$。
又因为 $i,j$ 两个指针总共向后移动小于 $2n$,因此时间复杂度为 $O(n)$。
代码是 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。
设两个指针 $i$ 和 $j$ 表示当前文本串 $t$ 匹配到 $i$,模式串 $s$ 匹配到 $j$。
KMP 的思想为:找一个最大的 $j$ 满足 $suf(pre(t,i),j)=pre(s,j)$,当 $j=\vert s\vert$ 时意味着匹配到了模式串 $s$。
考虑 $i$ 由 $i-1$ 向右移动一位,若 $s[i]=t[i]$,则显然 $++j$ 即可。
但是若 $s[i]\not=t[i]$,则新的 $j'$ 与原先的 $j$ 一定满足 $j'<j$ 且 $pre(s,j'-1)$ 是 $pre(s,j)$ 的 border。
这是因为 $pre(s,j'-1)$ 显然是 $s$ 的前缀和 $pre(s,j)$ 的后缀,又因为 $j'<j$ 所以这个 $s$ 的前缀也是 $pre(s,j)$ 的前缀。
因此相当于找一个 $pre(s,j)$ 的最长的 border $pre(s,j'-1)$ 满足 $t[i]=s[j']$。
由 border 的特殊性质 9,由长到短找到第一个满足 $t[i]=s[j']$ 的 border 即为最长的 border。
因为找的都是模式串 $s$ 的前缀的 border,与文本串 $t$ 无关,所以可以预处理 $nxt[j]$ 表示 $pre(s,j)$ 的最长的 border 的长度。
因此 KMP 的 next 函数的实质是:$nxt[j]=\vert lb(pre(s,j))\vert$,即 $s$ 的前缀的最长 border 的长度。
如何求 next 函数?设两个指针 $i$ 和 $j\ (j<i)$ 表示当前已求出 $nxt[1\cdots i-1]$,且 $nxt[i-1]=j$,现在需要求 $nxt[i]$。
由 border 的特殊性质 6,若 $nxt[i-1]=j,\ s[i]=s[j+1]$,则 $nxt[i]=j+1$。
由 border 的特殊性质 9,$pre(s,i-1)$ 的所有 border 是 $nxt[i-1],nxt[nxt[i-1]],nxt[nxt[nxt[i-1]]],\cdots$。
所以从 $j\leftarrow nxt[i-1]$ 开始看 $s[i]=s[j+1]$ 是否成立,不成立就 $j\leftarrow nxt[j]$ 用 next 函数往回退,找出最长 border。
虽然 KMP 的预处理 next 函数过程和模式匹配过程都有进($++j$)有退($j\leftarrow nxt[j]$),但是由于每次进都是 $++j$,进的次数最多为 $\vert s\vert+\vert t\vert$,$j$ 非负,所以 $\Delta j\le2(\vert s\vert+\vert t\vert)$,因此均摊后总时间复杂度是线性的,为 $O(\vert s\vert+\vert t\vert)$。
根据整个 KMP 算法可以显然发现 next 函数满足一个非常非常非常重要的不等式:$nxt[i]\le nxt[i-1]+1$ 恒成立。
注意类似 next 函数或者能够抽象成类似 next 函数的函数 $f$(定义在字符串上或者能够抽象成在字符串上且从 $i$ 开始不断 $i\leftarrow f[i]$ 则 $i$ 严格单调递减的函数)若也满足不等式 $f[i]\le f[i-1]+1$ 恒成立,则 $f[i]$ 的计算也可以用 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 树。
$s$ 的失配树是将 $nxt[i]$ 视为 $i$ 的父节点,由 $0\sim n$ 共 $n+1$ 个点构成的一棵树,$0$ 为根节点,深度为 $0$。
由于 $i$ 只有一个 $nxt[i]$ 且 $nxt[i]<i$,所以显然是一棵树。
失配树的特殊性质:
- 点 $i$ 的所有祖先都是 $pre(s,i)$ 的 border,点 $i$ 的父亲是 $lb(pre(s,i))$。
- 没有祖先关系的两个点 $i,j$ 没有 border 关系。
- 祖先的节点编号一定小于后代的节点编号。
性质非常显然易证。
KMP 预处理 $nxt[i]$ 可看成:从 $j\leftarrow fa[i-1]$ 往上走,找到第一个满足 $s[i]=s[j+1]$ 的点 $j+1$,并 $fa[i]\leftarrow j+1$。
KMP 模式匹配过程可看成:从 $j\leftarrow fa[i-1]$ 往上走,找到第一个满足 $t[i]=s[j+1]$ 的点 $j+1$,并 $fa[i]\leftarrow j+1$。
失配树是 KMP 的核心,KMP 看似难以理解,考虑成失配树上的操作会很容易明白,很多 KMP 难题考虑失配树直接变得很简单。
题意:给定 $s$,$m$ 次询问,每次给 $p,q$ 求 $\vert lcb(pre(s,p),pre(s,q))\vert$。$1\le\vert s\vert\le10^6,\ 1\le m\le10^5$。
由失配树的特殊性质 1,答案显然是失配树上 $p$ 和 $q$ 的 lca,当然若 $p,q$ 是祖先后代关系应该是 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:给定 $s$,求最短的 $t$,使得 $s$ 是若干个 $t$ 拼成的字符串的前缀。$1\le\vert s\vert,\vert t\vert\le10^7$。
题意 2:给定 $s$,求字典序最小的 $t$,使得 $s$ 是若干个 $t$ 拼成的字符串的前缀。$1\le\vert s\vert,\vert t\vert\le10^7$。
题意 3:给定 $s$,求最短的 $t$,使得 $s$ 恰好是若干个 $t$ 拼成的字符串。$1\le\vert s\vert,\vert t\vert\le10^7$。
题意 4:给定 $s$,求字典序最小的 $t$,使得 $s$ 恰好是若干个 $t$ 拼成的字符串。$1\le\vert s\vert,\vert t\vert\le10^7$。
题意 5:给定 $s$,求最短的 $t$,使得 $s$ 是若干个 $t$ 拼成的字符串的子串,若有多解输出任意一个。$1\le\vert s\vert,\vert t\vert\le10^7$。
题意 6:给定 $s$,求最短的 $t$,使得 $s$ 是若干个 $t$ 拼成的字符串的子串,若有多解输出字典序最小的解。$1\le\vert s\vert,\vert t\vert\le10^7$。
不难发现这 6 个题意实际都是与 $s$ 的最短周期有关,答案都与 $pre(s,\vert s\vert-\vert lb(s)\vert)$ 有关。
注意题意 3 和 4 还要判断 $\vert s\vert\bmod{(\vert s\vert-\vert lb(s)\vert))}=0$,题意 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
题意:给定 $s$,求字典序最小的 $t$,使得 $s$ 是若干个 $t$ 拼成的字符串的子串。$1\le\vert t\vert\le\vert s\vert\le10^7$。
虽然写的是“KMP 练习 2”但是不知道是否能用 KMP 或者其他什么神仙做法做到线性复杂度……
给一个可能 hack 掉很多做法的数据:$s$ 为 $\verb@aabaa@$,答案是 $\verb@aaaab@$,并不是 $\verb@aab@$。
暴力是先求出每个串的前缀的最小表示(参考 P5334 [JSOI2019]节日庆典)再找合法的取字典序最小的,用 SA 可以做到一个 $\log$。
- KMP 练习 3
给定文本串 $s$ 和模式串 $t$,求最多能模式匹配互不重叠的模式串 $t$ 的个数。$1\le\vert s\vert,\vert t\vert\le10^7$。
显然从左到右贪心能选就选。
- KMP 练习 4
题意:给定 $s$,对每个 $pre(s,i)$ 都求最长的 $t$,使得 $\vert t\vert<i$ 且 $pre(s,i)$ 是若干个 $t$ 拼成的字符串的前缀。$1\le\vert s\vert\le10^7$。
实际就是求 $s$ 的每个前缀的最长周期,答案显然为 $pre(s,i)$ 的最短 border,在失配树上就是 $i$ 的所有祖先中离 $0$ 最近的那个点。
可以先构造出失配树再 DFS 一遍,也可以将 $fa[i]$ 进行路径压缩优化,时间复杂度为 $O(\vert s\vert)$。
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
题意:给定 $n\times m$ 的字符矩阵,求最小连续子矩阵,使得该子矩阵无限复制扩张后的矩阵包含原矩阵。$1\le n,m\le3\times10^3$。
实际上就是 KMP 练习 1 的题意 1 的字符串变成了二维的情况,将每一行和每一列都当成一个整体(最好 Hash 成一个数)即可。
对 $n$ 行整体求出最短周期 $p$,对 $m$ 列整体求出最短周期 $q$,则答案显然为 $p\times q$ 的字符矩阵。
KMP 时间复杂度为 $O(nm)$,用 Hash 优化比较运算能做到 $O(n+m)$,但输入量为 $O(nm)$ 所以总时间复杂度还是 $O(nm)$。
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
题意:给定 $s$,对每个 $pre(s,i)$ 求长度不超过 $\dfrac{i}{2}$ 的 border 数量。$1\le\vert s\vert\le10^7$。
实际上就是求失配树上 $i$ 的祖先中第一个编号不超过 $\dfrac{i}{2}$ 的点的深度,直接暴力倍增就可以做到 $O(\vert s\vert\log{\vert s\vert})$。
令 $Nxt[i]$ 表示 $pre(s,i)$ 的长度不超过 $\dfrac{i}{2}$ 的最长 border 的长度,则不等式 $Nxt[i]\le Nxt[i-1]+1$ 恒成立。
证明:
因为 $Nxt[i]\le\dfrac{i}{2},\ Nxt[i-1]\le\dfrac{i-1}{2}$,所以 $Nxt[i]-1\le\dfrac{i}{2}-1\le\dfrac{i-1}{2}$。
又因为 $Nxt[i]-1$ 是 $pre(s,i-1)$ 某个 border 的长度,$Nxt[i-1]$ 是 $pre(s,i-1)$ 长度不超过 $\dfrac{i-1}{2}$ 的最长 border 的长度,所以 $Nxt[i]-1\le Nxt[i-1]$ 即 $Nxt[i]\le Nxt[i-1]+1$。
证毕。
因此用 KMP 均摊线性求出 $Nxt[i]$ 即可,注意每次进后若超过 $\dfrac{i}{2}$ 要回退一次,时间复杂度为 $O(\vert s\vert)$。
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
题意:给定文本串 $s$,求最短模式串 $t$,使得在 $s$ 中出现过的所有的 $t$ 能将 $s$ 的每个位置都至少覆盖一次。$1\le\vert s\vert,\vert t\vert\le10^7$。
$s$ 的首尾字符都要被 $t$ 覆盖到所以显然 $t$ 是 $s$ 的 border,所以答案只能是失配树上 $0\rightarrow\vert s\vert$ 这条链上第一个合法的节点。
由失配树的特殊性质 1,$pre(s,i)$ 是 $i$ 的所有后代的 border,所以 $i$ 的子树内所有节点排序后相邻两个节点的差的最大值(即最大间隔 $maxgap$)就是以 $pre(s,i)$ 作为模式串 $t$,$s$ 中相邻两个模式串 $t$ 的距离的最大值,只要 $maxgap\le i$ 显然是合法的。
随着点的不断删除 $maxgap$ 显然单调不减,因此只需要一种数据结构支持删除 $i$,查询 $i$ 的前驱和后继,可以考虑双向链表。
因此维护当前 $i$ 的子树内的点即可,有意思的是由失配树的特殊性质 3,$i$ 的祖先即使不删除显然也不会影响答案(懒)。
由于每个点最多只会被删除一次,所以时间复杂度为 $O(\vert s\vert)$。
当然这道题还有一种不用失配树的更简单的做法:直接考虑 DP,设 $dp[i]$ 表示 $pre(s,i)$ 的答案,实质是合法最短 border 长度。
显然 $dp[i]$ 只可能是 $dp[nxt[i]]$ 或 $i$,它能由 $dp[nxt[i]]$ 转移当且仅当存在一个 $j$ 满足 $dp[j]=dp[nxt[i]],\ i-nxt[i]\le j$。
因此开一个桶 $b[i]$ 来存储 $j$ 的值,表示以 $pre(s,i)$ 作为模式串 $t$,文本串为 $pre(s,j)$ 合法的最大的 $j$。
时间复杂度也是 $O(\vert s\vert)$。
第一份代码是失配树的,第二份代码是 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
题意:给定字符集 $\Sigma$ 和长为 $n$ 仅由 $\Sigma$ 中的字符构成的 $s$,求长为 $m$ 仅由 $\Sigma$ 中的字符构成的 $t$ 的个数,使得 $s$ 是 $t$ 的子串,答案对 $10^9+7$ 取模。$1\le\vert\Sigma\vert\le10^4,\ 1\le n\le100,\ n\le m<2^{128}$。
设 $dp[i][j]$ 表示当前 $t$ 构造到 $i$,$s$ 匹配到 $j$ 的答案,枚举 $i+1$ 处的字母,此时 $s$ 匹配到 $j'$,则 $dp[i+1][j']+=dp[i][j]$。
注意 $dp[i][n]$ 不转移,相当于 $s$ 第一次出现时统计答案,显然不会算重,最终答案为 $\sum{dp[i][n]}$,这样能做到 $O(\vert\Sigma\vert n^2m)$。
$dp[i][\cdots]$ 只能转移到 $dp[i+1][\cdots]$,$j'$ 只与 $s$ 有关与 $t$ 无关,$n$ 很小但 $m$ 巨大,这都启发我们矩阵快速幂。
将 $dp[i][\cdots]$ 看成 $1\times n$ 的行向量,将转移看成 $n\times n$ 的列向量,根据 $s$ 预处理出转移矩阵即可。
最终答案的计算可以再加一列记录 $dp[i][n]$ 的前缀和,也可以容斥计算,$\sum{dp[m][0\cdots n-1]}$ 显然是 $s$ 不是 $t$ 的子串的答案。
预处理 $O(\vert\Sigma\vert n^2)$,矩阵快速幂 $O(n^3\log{m})$,所以总时间复杂度为 $O(\vert\Sigma\vert n^2+n^3\log{m})$。
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
题意:给定 $s,t$,对每个 $j$ 都求出 $j$ 是满足 $pre(suf(s,i),j)=pre(t,j)$ 的最大值的 $i$ 的个数。$1\le\vert s\vert,\vert t\vert\le10^7$。
问题等价于将 $s$ 的所有后缀与 $t$ 匹配,后缀的首字符和 $t$ 的首字符对齐,对每个 $j$ 求匹配长度恰好为 $j$ 的后缀个数。
看起来似乎很难用 KMP 解决,但可以用差分思想去转化,若问题是匹配长度至少为 $j$ 呢?
若 $pre(s,i)$ 匹配长度为 $j$,则以 $i-j+1,i-nxt[j]+1,\cdots$ 为首字符的后缀的匹配长度分别至少为 $j,nxt[j],\cdots$。
因此相当于失配树上将 $j\rightarrow 0$ 这条链上的点的答案都 $+1$,先打标记最后遍历失配树求答案,时间复杂度为 $O(\vert s\vert+\vert t\vert)$。
- KMP 练习 10
题意:给定文本串 $s$ 和模式串 $t$,将 $s$ 中的 $t$ 不断删除,求最后的 $s$,注意删除一个 $t$ 后两边会拼在一起。$1\le\vert s\vert,\vert t\vert\le10^7$。
用一个栈来存储当前的 $s$,再记录当前的 $s$ 的前缀的匹配长度 $Nxt[i]$ 即可。
- KMP 练习 11
题意:给定 $s$,可以将 $s$ 中连续几个相同的部分压缩成一个,操作可以进行若干次,求最终 $s$ 的长度最小值。$1\le\vert s\vert\le500$。
考虑区间 DP,实际上压缩就是 KMP 练习 1 的题意 3 的问题,每个区间(子串)都找最短循环节即可,时间复杂度为 $O({\vert s\vert}^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
题意:给定文本串 $s$ 和模式串 $t$,判断 $t$ 是否在 $s$ 中出现恰好一次。$1\le\vert s\vert,\vert t\vert\le10^7$。
直接匹配求次数即可。
- KMP 练习 13
题意:给定 $s$,求它的最长前缀 $t$(不能为空但可以为 $s$)使得 $t$ 翻转后恰好也是 $s$ 的后缀。$1\le\vert s\vert\le10^7$。
将 $s$ 翻转并和原来的 $s$ 求 LCP 即可。
- KMP 练习 14
题意:给定 $n\times m$ 的文本字符矩阵和 $p\times q$ 的模式字符矩阵,求出现次数。$1\le n,m,p,q\le3\times10^3$。
与 KMP 练习 5 的做法类似,二维 Hash 后直接匹配即可,时间复杂度为 $O(nm+pq)$。
- KMP 练习 15
题意:给定仅由小写字母和问号构成的文本串 $s$ 和仅由小写字母构成的模式串 $t$,问号可以替换为任意一个小写字母,求 $t$ 在 $s$ 中的出现次数的最大值。$1\le\vert s\vert,\vert t\vert\le3\times10^3$。
与 KMP 练习 8 的做法类似,由于 $\vert s\vert$ 与 $\vert t\vert$ 同阶,即使预处理转移矩阵时间复杂度仍然不会变。
因此直接暴力 DP 求解即可,时间复杂度为 $O(\Sigma\vert s\vert\vert t\vert)$。
- KMP 练习 16
题意:给定两段墙 $s$ 和 $t$,求 $s$ 墙中与 $t$ 墙顶部形状相同的区间数。$1\le\vert s\vert\le10^7$。
形状相同即差分后子串相同,然后求子串出现次数即可,注意 $t=1$ 的情况需要特判。
- KMP 练习 17
题意:给定 $s$,求最长的 $t$,满足 $t$ 既是 $s$ 的前缀也是 $s$ 的后缀还是 $s$ 的子串(不含前后缀)。$1\le\vert s\vert\le10^7$。
首先答案显然一定是 $s$ 的某个 border,其次子串必然是某个后缀的前缀。
- KMP 练习 18
题意:
- KMP 练习 19
题意:
- KMP 练习 20
题意:
-
- 例题:
四、exKMP
1、exKMP 算法
exKMP,又称扩展 KMP,Z 函数,Z Algorithm。
实际上 KMP 与 exKMP 的思想类似,过程相像,学懂了 KMP 后 exKMP 很容易理解。
设 $n=\vert s\vert$,定义函数 $z[i]=\vert lcp(s,s[i\cdots n])\vert$,即 $s$ 与以 $s[i]$ 开头的后缀的最长公共前缀的长度。
特别地,$z[1]$ 的取值为 $0$ 还是 $n$ 需要看题意,这里的前缀是否必须是真前缀(不包含 $s$ 本身)。
$z$ 被称为 $s$ 的 Z 函数,$s[i\cdots i+z[i]-1]$ 被称为 $i$ 的匹配段,又称 $i$ 的 Z-box。
计算 $z[i]$ 的过程与 $nxt[i]$ 类似,也需要利用 $z[1\cdots i-1]$,从 $2$ 到 $n$ 顺次计算。
我们需要维护右端点最靠右的 Z-box $s[l\cdots r]$,显然它也是 $pre(s,r-l+1)$,计算 $z[i]$ 时保证 $l\le i$,令 $l,r$ 的初值为 $0$。
若 $i\le r$,显然 $s[i\cdots r]=s[i-l+1\cdots r-l+1]$,因此若 $z[i-l+1]\le r-i+1$,则 $z[i]\leftarrow z[i-l+1]$,否则 $z[i]\leftarrow r-i+1$ 后暴力向右扩展。若 $i>r$,直接暴力向右扩展。最后注意用求出的 $z[i]$ 更新 $l,r$ 即可。
这个网站 是对 exKMP 预处理 $z$ 函数的演示,模式匹配的过程也跟预处理的过程非常像,画个图不难自推。
考虑 $r$ 只会向前进不会向后退,$r\le n$,所以时间复杂度均摊线性为 $O(n)$。
不难发现实质上 KMP 是 $s$ 与 $s$ 的前缀的最长后缀匹配,exKMP 是 $s$ 与 $s$ 的后缀的最长前缀匹配。
它们都能处理子串模式匹配问题,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 都能做”?
因为在不知道原串的前提下,只知道 $nxt[i]$ 可以求出 $z[i]$,只知道 $z[i]$ 也可以求出 $nxt[i]$,即它们之间可以相互转化。
Dan Gusfield 对其进行了详细证明: