字符串练习笔记
一、字符串前置知识
注:若无特殊说明则默认字符串的下标从 $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 的特殊性质:
- 若 $pre(s,k)$ 是 $s$ 的 border,则 $\vert s\vert-k$ 是 $s$ 的 period。
- 若 $k
- 若 $k
- 若 $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 环环相扣,由长到短被一条链串在了一起。
其中第 $1$ 条是 border 与 period 的关系,第 $2$ 条和第 $3$ 条为互逆命题,第 $4\sim 6$ 条是 border 的传递性。
这些特殊性质都非常简单,读者自证不难,举个栗子画个图就很容易证明。
二、kmp
设两个指针 $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)$。
- P3375 【模板】KMP字符串匹配
模板题,显然 $s1$ 是文本串,$s2$ 是模式串。
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
1