字符串学习笔记 (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 的特殊性质:

  1. 若 $pre(s,k)$ 是 $s$ 的 border,则 $\vert s\vert-k$ 是 $s$ 的 period。
  2. 若 $k$ 是 $s$ 的 period,则 $pre(s,\vert s\vert-k)$ 是 $s$ 的 border。
  3. 若 $pre(s,k)$ 是 $s$ 的 border,$\vert s\vert\bmod k=0$,则 $s$ 是由 $\dfrac{\vert s\vert}{k}$ 个 $pre(s,k)$ 拼成的。
  4. 若 $s$ 是由 $\dfrac{k}{s}$ 个 $pre(s,k)$ 拼成的,则 $pre(s,k)$ 是 $s$ 的 border,$\vert s\vert\bmod k=0$。
  5. 若 $k<j<i$,$pre(s,j)$ 是 $pre(s,i)$ 的 border,则 $pre(s,j-k)$ 是 $pre(s,i-k)$ 的 border。
  6. 若 $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。
  7. 若 $s$ 是 $t$ 的 border,$t$ 是 $r$ 的 border,则 $s$ 是 $r$ 的 border。
  8. 若 $s$ 是 $r$ 的 border,$t$ 是 $r$ 的 border,$\vert s\vert<\vert t\vert$,则 $s$ 是 $t$ 的 border。
  9. $lb(s),lb(lb(s)),lb(lb(lb(s))),\cdots$ 构成了 $s$ 的所有 border,即 $s$ 的所有 border 环环相扣,由长到短被一条链串在一起。
  10. $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 }
View Code

三、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 }
View Code

2、KMP 与失配树

失配树,又称 next 树,fail 树,是只挂了一个串的 ACAM 的 fail 树。

$s$ 的失配树是将 $nxt[i]$ 视为 $i$ 的父节点,由 $0\sim n$ 共 $n+1$ 个点构成的一棵树,$0$ 为根节点,深度为 $0$。

由于 $i$ 只有一个 $nxt[i]$ 且 $nxt[i]<i$,所以显然是一棵树。

失配树的特殊性质:

  1. 点 $i$ 的所有祖先都是 $pre(s,i)$ 的 border,点 $i$ 的父亲是 $lb(pre(s,i))$。
  2. 没有祖先关系的两个点 $i,j$ 没有 border 关系。
  3. 祖先的节点编号一定小于后代的节点编号。

性质非常显然易证。

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 }
View Code

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 }
View Code

代码是 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 }
View Code
 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 }
View Code
  • 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 }
View Code
  • 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 }
View Code
  • 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 }
View Code
  • 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 }
View Code
 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 }
View Code
  • 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 }
View Code
  • 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)$。

View Code
  • KMP 练习 10

题意:给定文本串 $s$ 和模式串 $t$,将 $s$ 中的 $t$ 不断删除,求最后的 $s$,注意删除一个 $t$ 后两边会拼在一起。$1\le\vert s\vert,\vert t\vert\le10^7$。

用一个栈来存储当前的 $s$,再记录当前的 $s$ 的前缀的匹配长度 $Nxt[i]$ 即可。

View Code
  • 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 }
View Code
  • KMP 练习 12

题意:给定文本串 $s$ 和模式串 $t$,判断 $t$ 是否在 $s$ 中出现恰好一次。$1\le\vert s\vert,\vert t\vert\le10^7$。

直接匹配求次数即可。

题意:给定 $s$,求它的最长前缀 $t$(不能为空但可以为 $s$)使得 $t$ 翻转后恰好也是 $s$ 的后缀。$1\le\vert s\vert\le10^7$。

将 $s$ 翻转并和原来的 $s$ 求 LCP 即可。

题意:给定 $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 }
View Code

2、KMP 与 exKMP 的相互转化

初学时可能会有疑问,为什么“KMP 能做的题 exKMP 都能做,exKMP 能做的题 KMP 都能做”?

因为在不知道原串的前提下,只知道 $nxt[i]$ 可以求出 $z[i]$,只知道 $z[i]$ 也可以求出 $nxt[i]$,即它们之间可以相互转化。

Dan Gusfield 对其进行了详细证明:

3、exKMP 练习

posted @ 2022-02-11 09:50  jhqqwq  阅读(88)  评论(1编辑  收藏  举报