KMP
引入
kmp 需要处理的问题是“字符串匹配问题”,具体问题如下:
【模板】KMP 字符串匹配,AC 代码提交记录
给出两个字符串 \(s_1\) 和 \(s_2\),若 \(s_1\) 的区间 \([l, r]\) 子串与 \(s_2\) 完全相同,则称 \(s_2\) 在 \(s_1\) 中出现了,其出现位置为 \(l\)。
现在请你求出 \(s_2\) 在 \(s_1\) 中所有出现的位置。
首先,这个问题有一个很显然的暴力做法,就是枚举 \(s_1\) 中的所有位置,然后向后枚举 \(|s_2|\) 位直接尝试匹配。
取 s1=ababaabaabac
,s2=abaabac
,暴力匹配的流程如下:
编号 | a | b | a | b | a | a | b | a | a | b | a | c | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | a | b | a | a | b | a | c | 前三位匹配成功,第四位失配 | |||||
2 | a | b | a | a | b | a | c | 第一位失配 | |||||
3 | a | b | a | a | b | a | c | 前六位匹配成功,第七位失配 | |||||
4 | a | b | a | a | b | a | c | 第一位失配 | |||||
5 | a | b | a | a | b | a | c | 第一位匹配成功,第二位失配 | |||||
6 | a | b | a | a | b | a | c | 匹配成功 |
这个算法时间复杂度 \(O(|s_1||s_2|)\),不够优秀。如果考虑优化,我们会发现:
- 两个字符串 \(s_1\) 和 \(s_2\) 都至少遍历一遍。
- 每次失配之后,都是从头枚举,想要优化就必须从之前的失配中吸取经验。
前缀函数
我们先来看一个东西:
定义一个字符串 \(s\) 的 border 为 \(s\) 的一个非 \(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
对于 \(s_2\),你还需要求出对于其每个前缀 \(s'\) 的最长 border \(t'\) 的长度。
这是 P3375 中提到的一个相关定义,接下来将详细解析所有与之相关的字符串定义。
- 【子串】:字符串 \(s\) 中连续的一部分,具体而言:设 \(1\leq i\leq j\leq s.length()\),字符串 \(s\) 的第 \(i\) 位到第 \(j\) 位,即 \(s[i],s[i+1],\cdots,s[j]\) 所形成的一个字符串,就称为子串,用 \(s[i...j]\) 表示。
- 【真子串】:很好理解,\(s\) 的所有子串中,只有 \(s\) 本身不是 \(s\) 的真子串,毕竟 \(s\) 就是它本身嘛,算一个“假的”子串。
- 【前缀】:这个也很好理解,字符串 \(s\) 头上的一部分就是前缀,即 \(s[1...i]\)。
- 【真前缀】:和真子串一样,所有前缀中只有 \(s\) 本身不是真前缀。
- 与之相对应的,还有【后缀】、【真后缀】这两个概念,此处就不再赘述。
接下来我们举一个例子,来介绍题干中 border
的概念。
我们取 s=ababa
,很显然,a
既是 \(s\) 的一个真前缀,也是 \(s\) 的一个真后缀,于是 a
就是符合题干的一个 border
。
符合条件的 border
还有 aba
,于是题干中所求的【最长 border
】的长度也就是 \(3\)。
对于字符串 \(s\),其【前缀函数】\(\pi[i]\) 定义为:子串 \(s[1...i]\) 的最长 border
长度。也就是说,题干中所要求出的【每个前缀 \(s'\) 的最长 border
\(t'\) 的长度】就是字符串 \(s\) 的所有前缀函数值。
比如,我们取 s=abaabac
,其前缀函数的值计算如下:
\(i\) | \(s[i]\) | \(\pi[i]\) | 最长 border |
---|---|---|---|
1 | a | 0 | 不存在真前缀和真后缀 |
2 | b | 0 | a 和 b 不一样 |
3 | a | 1 | a |
4 | a | 1 | a |
5 | b | 2 | ab |
6 | a | 3 | aba |
7 | c | 0 | a 和 c 不一样 |
前缀函数的计算方法我们将在后文介绍。
kmp 与前缀函数
上一节介绍了前缀函数的概念,那么它和字符串匹配问题有什么关系呢?
设已知字符串 \(s_1\) 和 \(s_2\),其中 \(s_2\) 的长度为 \(sz\)。
如果 \(s_2\) 在 \(s_1\) 中出现了,那么必然存在一个位置 \(i\),满足 \(s_2 = s_1[(i-sz+1)...i]\),即子串 \(s_1[1...i]\) 长度为 \(sz\) 的后缀恰好和 \(s_1\) 相等。
- 比如我们可以取
s1=ababaabaabac
,s2=abaabac
。因为子串 \(s_1[1...12]\) 有一个长度为 \(7\) 的后缀abaabac
,恰好等于 \(s_2\),所以我们就可以判断 \(s_2\) 在 \(s_1\) 中出现了。
我们可以令 \(T=s_2+s_1\),即将 \(s_1\) 直接接在 \(s_2\) 后面。
那么字符串匹配就转换为了:若存在一个位置 \(i\),使得 \(T[1...i]\) 具有长度为 \(sz\) 的 border
,那么就匹配成功了。
如果我们能找到一种合理的方式计算前缀函数,并且找到所有长度的 border
,那么这个问题就得到了解决。
如何计算前缀函数?
对于字符串 \(s\),如果我们想计算其前缀函数,一种暴力的方法是:一重循环枚举子串 \(s[1...i]\),一重循环枚举子串的真前缀 \(s[1...j]\),然后和对应的真后缀比较,时间复杂度 \(O(|s|^3)\)。
我们可以考虑递推的方式来优化这个过程。
取 s=abcxabcwabcxabcx
,假设我们已经计算出了前 14 位的前缀函数 \(\pi[i]\)。
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
\(s[i]\) | a | b | c | x | a | b | c | w | a | b | c | x | a | b | c | x |
\(\pi[i]\) | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
- 在考虑第 15 位的时候,我们发现 \(s[1...14]\) 有一个长度为 6 的
border
,即 \(s[1...6]=s[9...14]\),并且 \(s[7]=s[15]\),于是这个border
可以再次延伸一格,变为abcxabc
,即 \(\pi[15]=\pi[14]+1=7\)。 - 在考虑第 16 位的时候,我们依然想按照上述的方法,比较 \(s[\ \pi[i-1]+1\ ]\) 和 \(s[i]\)。
- 结果发现 \(s[8]=w\neq s[16]=x\)。也就是说,
abcxabcx
不是 \(s[1...16]\) 的一个border
。 - 此时如果按照暴力的做法,应该枚举
bcxabcx
是不是一个border
。但其实,我们还可以再利用一次 \(\pi[]\)。 - 仅仅通过 \(\pi[15]\),我们就可以快速知道 \(s[1...7]=s[9...15]\)。如果我们此时取 \(s[1...\pi[7]]\),即
abc
,由前缀函数的定义我们可以知道:abc
既是 \(s[1...7]\) 的真前缀,也是 \(s[1...7]\) 的真后缀,既是 \(s[9...15]\) 的真前缀,也是 \(s[9...15]\) 的真后缀。于是我们就找到了一个长度第二大的border
。 - 此时再比较 \(s[\pi[7]+1]\) 和 \(s[16]\) 即可,循环执行此流程, 即可求出 \(\pi[]\)。
- 结果发现 \(s[8]=w\neq s[16]=x\)。也就是说,
接下来展示求 \(\pi[i]\) 的代码,代码中变量名采用 nxt[i]
。
int nxt[MAXN];
// I.N.
string s2; cin >> s2;
int len2 = s2.length();
s2 = " " + s2;
// 初值
nxt[1] = 0;
for (int i=2, j=0; i <= len2; ++i) {
// 如果s2[i]==s2[j+1],那么原来的border就可以延伸为s2[1...j+1]
// 否则,找到一个长度更小的border,看看是不是能延伸
while (j && s2[i] != s2[j+1]) { j = nxt[j]; }
// 原来的border延伸一位
if (s2[i] == s2[j+1]) { ++j; }
// 记录求出的前缀函数值
nxt[i] = j;
}
kmp 的流程
在计算了前缀函数,并且找到所有长度的 border
之后,字符串匹配问题也就得到了解决。
按照前文所述,将原问题转换为:【若存在一个位置 \(i\),使得 \(T[1...i]\) 具有长度为 \(sz\) 的 border
,那么就匹配成功了。】
代码如下:
void kmp()
{
for (int i=1, j=0; i <= len1; ++i) {
// 假想一个拼接的字符串T=s2+s1,接下来的两行代码和求前缀函数就一样了
while (j && s1[i]!=s2[j+1]) { j = nxt[j]; }
if (s1[i] == s2[j+1]) { ++j; }
// 因为没有真的拼接,j最大也就等于s2的长度,此时就是匹配上了,输出答案
if (j == len2) { cout << i-len2+1 << endl; }
}
}
前缀函数与字符串的周期
UVA1328 Period,AC 代码提交记录。
设循环节的长度为 \(k\),那么 \(s[1...i]\) 有长度为 k 的循环节的充要条件为:\(s[1...i-k+1] = s[1+k...i]\),并且 \(i\mod k=0\)。
即:前后各自长度为 \(i-k+1\) 的真前后缀相等,并且 \(s[1...i]\) 恰好能容下整数个循环节。
当枚举到位置 \(i\) 的时候,求出它的 \(nxt[i]\)。
此时 \(s[1 ... nxt[i]] = s[i-nxt[i]+1 ... i]\) 是已经确定的。
并且由于 \(nxt[i]\) 是最长相同前后缀的长度,此时的循环节必然是最小的那一个。
只需要判断是否满足 \(i\mod (i-nxt[i])=0\) 即可。
如果满足,此时周期串的长度为 \(i\),循环节的长度为 \(i-nxt[i]\),循环节的个数为 \(\displaystyle\frac{i}{(i-nxt[i])}\)。
参考代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int t, n, nxt[MAXN];
string s;
int main()
{
while ( (cin>>n) && (n!=0) ) {
cin >> s; s=" "+s;
cout << "Test case #" << ++t << endl;
nxt[1]=0;
for (int i=2, j=0; i <= n; ++i) {
while (j && s[i]!=s[j+1]) { j=nxt[j]; }
if (s[i] == s[j+1]) { ++j; }
nxt[i] = j;
if ( (nxt[i]!=0) && (i%(i-nxt[i])==0)) { cout << i << " " << i/(i-nxt[i]) << endl; }
}
cout << endl;
}
return 0;
}