KMP
引入
kmp 需要处理的问题是“字符串匹配问题”,具体问题如下:
给出两个字符串 \(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\) 中连续的一部分,具体而言:设 \(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\) 本身不是真前缀。
- 与之相对应的,还有【后缀】、【真后缀】这两个概念,此处就不再赘述。
定义一个字符串 \(s\) 的 border 为 \(s\) 的一个非 \(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
对于 \(s_2\),你还需要求出对于其每个前缀 \(s'\) 的最长 border \(t'\) 的长度。
这是 P3375 中提到的一个相关定义。接下来我们举一个例子,来介绍题干中 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 不一样 |
如何计算前缀函数
对于字符串 \(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]\) 的代码。
vector<int> prefixFunction(string s)
{
vector<int> pi(s.length());
// 初值 pi[1] = 0;
for (int i = 1; i < s.length(); ++i) {
int j = pi[i-1];
// 如果 s[j]==s[i],那么原来的border就可以延伸为 s[1...j]
// 否则,找到一个长度更小的 border,看看是不是能延伸
while (j && s[j]!=s[i]) { j = pi[j-1]; }
// 原来的 border延伸一位
if (s[j] == s[i]) { ++j; }
// 记录求出的前缀函数值
pi[i] = j;
}
return pi;
}
根据 \(\pi[i]\) 的定义,\(\pi[i]-\pi[i-1]\leq1\),求前缀函数的过程可以视作不断 \(+1\) 的过程,\(\pi[i]\leq |s|\)。
而一旦失配,前方累计的长度会经历若干次“释放”,此时复杂度最劣的情况下,while (j && s[j]!=s[i]) { j = pi[j-1]; }
的循环可以视作不断地 \(-1\),复杂度最劣为 \(O(\max\{\pi[i]\})=O(|s|)\)。
因此,这个算法可以在线性的时间内求解前缀函数。
前缀函数与 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 + \texttt{\#} + s_1\),也就是将 \(s_1\) 直接接在 \(s_2\) 后面。那么字符串匹配就转换为了:若存在一个位置 \(i\),使得 \(T[1...i]\) 具有长度为 \(sz\) 的 border
,那么就匹配成功了。
在计算了前缀函数,并且找到所有长度的 border
之后,字符串匹配问题也就得到了解决。代码如下:
void kmp()
{
string s, t;
cin >> s >> t;
int n = s.length(), m = t.length();
vector<int> pi = prefixFunction(t+"#"+s);
for (int i = m+1; i <= n+m; ++i) {
if (pi[i] == m) { cout << i-2*m+1 << "\n"; }
}
for (int i = 0; i < m; ++i) { cout << pi[i] << " "; }
}
UVA1328 Period
设循环节的长度为 \(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\) 的时候,求出它的 \(\pi[i]\)。此时 \(s[1 ... \pi[i]] = s[i-\pi[i]+1 ... i]\) 是已经确定的。并且由于 \(\pi[i]\) 是最长相同前后缀的长度,此时的循环节必然是最小的那一个。
只需要判断是否满足 \(i\mod (i-\pi[i])=0\) 即可。如果满足,此时周期串的长度为 \(i\),循环节的长度为 \(i-\pi[i]\),循环节的个数为 \(\displaystyle\frac{i}{(i-\pi[i])}\)。
AC 代码
#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;
}