字符串练习笔记
一、字符串前置知识
注:若无特殊说明则默认字符串的下标从 $1$ 开始。
长度:$\vert s\vert$ 表示字符串 $s$ 的长度。
字符集:$\vert\Sigma\vert$ 表示字符集的大小,大多数字符串题只有小写字母 $\vert\Sigma\vert=26$。
模式串与文本串:从文本串中进行模式匹配寻找模式串。
子串:从原串中选取连续的一段字符串,空串也是子串。
前缀:$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 border:若 $0 最长 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$ 条是 border 与 period 的关系,第 $2$ 条和第 $3$ 条为互逆命题,第 $4\sim 6$ 条是 border 的传递性。 这些特殊性质都非常简单,读者自证不难,举个栗子画个图就很容易证明。 设两个指针 $i$ 和 $j$ 表示当前文本串 $t$ 匹配到 $i$,模式串 $s$ 匹配到 $j$。 kmp 的核心思想为:找一个最大的 $j$ 满足 $suf(pre(t,i),j)=pre(s,j)$。 考虑 $i$ 由 $i-1$ 向右移动一位,若 $s[i]=t[i]$,则显然 $++j$ 即可。 但是若 $s[i]\not=t[i]$,则新的 $j'$ 与原先的 $j$ 一定满足 $j' 这是因为 $pre(s,j'-1)$ 显然是 $s$ 的前缀和 $pre(s,j)$ 的后缀,又因为 $j' 因此相当于找一个 $pre(s,j)$ 的最长的 border $pre(s,j'-1)$ 满足 $s[j']=t[i]$。 根据 border 的第 $6$ 条特殊性质,由长到短找到第一个满足 $s[j']=t[i]$ 的 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
根据 border 的第 $3$ 条特殊性质,若 $nxt[i-1]=j,\ s[i]=s[j+1]$,则 $nxt[i]=j+1$。 根据 border 的第 $6$ 条特殊性质,$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)$。 模板题,显然 $s1$ 是文本串,$s2$ 是模式串。 1
二、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 }
AC Code